U4 第五步:AssetProvider 端口注入,移除 squad→template 依赖 #8

Merged
weichen merged 1 commits from feature/squad-asset-provider-2026-09-29 into dev 2026-09-29 06:35:17 +00:00
22 changed files with 123 additions and 59 deletions
+1 -1
View File
@@ -50,6 +50,7 @@ imports:
- squad - squad
- storage - storage
- stream - stream
- template
- textextract - textextract
- workflowdef - workflowdef
- workspace - workspace
@@ -288,7 +289,6 @@ imports:
- agent - agent
- config - config
- stream - stream
- template
- textextract - textextract
- workflowdef - workflowdef
storage: storage:
+1
View File
@@ -211,6 +211,7 @@ func NewService(cfg *config.Config) (*Service, error) {
return agent.NewBackend(name, cfg) return agent.NewBackend(name, cfg)
}, },
TaskPreparer: svc.squadTaskPreparer, TaskPreparer: svc.squadTaskPreparer,
Assets: templateAssetProvider{},
}) })
svc.parts = Parts{ svc.parts = Parts{
Pool: pool, Pool: pool,
+29
View File
@@ -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{}
+1 -1
View File
@@ -12,7 +12,7 @@ import (
func TestContextManager_InjectAndFormat(t *testing.T) { func TestContextManager_InjectAndFormat(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: tmpDir, WorkDir: tmpDir,
TaskKey: "research", TaskKey: "research",
+2 -2
View File
@@ -11,7 +11,7 @@ import (
func TestPrepareWorkspaceWritesDialogueContext(t *testing.T) { func TestPrepareWorkspaceWritesDialogueContext(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: dir, WorkDir: dir,
TaskKey: "system_reqs", TaskKey: "system_reqs",
@@ -104,7 +104,7 @@ func prepareDialogueWorkspace(t *testing.T, req squad.WorkspacePrepareRequest) s
ID: "s1", Title: "整理", Role: "organizer", Mode: "write", ID: "s1", Title: "整理", Role: "organizer", Mode: "write",
SubWorkspace: "tasks/01_organizer", Status: "pending", 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) t.Fatal(err)
} }
return req.WorkDir return req.WorkDir
+1 -1
View File
@@ -124,7 +124,7 @@ func TestWorkflowDriver_DynamicAdjustmentIntegration(t *testing.T) {
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
engine := NewWorkspaceEngine() engine := NewWorkspaceEngine(nil)
driver := NewWorkflowDriver(engine) driver := NewWorkflowDriver(engine)
evaluator := NewStepEvaluator() evaluator := NewStepEvaluator()
driver.SetEvaluator(evaluator) driver.SetEvaluator(evaluator)
+3 -3
View File
@@ -207,7 +207,7 @@ func TestDetectRepetition_NoFalsePositive(t *testing.T) {
func prepareGovWorkspace(t *testing.T, steps []squad.StepSpec) string { func prepareGovWorkspace(t *testing.T, steps []squad.StepSpec) string {
t.Helper() t.Helper()
dir := t.TempDir() dir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
_, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ _, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: dir, WorkDir: dir,
TaskKey: "intake", TaskKey: "intake",
@@ -272,7 +272,7 @@ func TestWorkflowDriver_GovernanceOnExecute(t *testing.T) {
{ID: "step_02", Role: "writer", Title: "成文", SubWorkspace: "tasks/02_draft"}, {ID: "step_02", Role: "writer", Title: "成文", SubWorkspace: "tasks/02_draft"},
} }
dir := prepareGovWorkspace(t, steps) 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}) res, err := driver.Execute(context.Background(), dir, &agent.StubBackend{}, squad.ExecuteOptions{Timeout: 10 * time.Second})
if err != nil || !res.Success { 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"}, {ID: "step_03", Role: "leader", Title: "终审", SubWorkspace: "tasks/03_review"},
} }
dir := prepareGovWorkspace(t, steps) dir := prepareGovWorkspace(t, steps)
driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil))
// 预算收紧为 2 步 → 第 3 步触发硬停 // 预算收紧为 2 步 → 第 3 步触发硬停
tight := squad.BudgetPolicy{MaxSteps: 2, MaxRepairRounds: 1, MaxAgentCalls: 10} tight := squad.BudgetPolicy{MaxSteps: 2, MaxRepairRounds: 1, MaxAgentCalls: 10}
+1 -1
View File
@@ -32,7 +32,7 @@ func (b *failFirstBackend) Execute(ctx context.Context, req agent.Request) (*age
func prepareDegradeWS(t *testing.T) (string, *squad.WorkflowDriver) { func prepareDegradeWS(t *testing.T) (string, *squad.WorkflowDriver) {
t.Helper() t.Helper()
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: tmpDir, WorkDir: tmpDir,
TaskKey: "degrade_test", TaskKey: "degrade_test",
+12 -5
View File
@@ -5,7 +5,6 @@ import (
"strings" "strings"
"time" "time"
"chat-agent-system/internal/template"
) )
// PlannerRequest 任务规划请求 // PlannerRequest 任务规划请求
@@ -36,14 +35,15 @@ type PlannerResult struct {
// AdaptivePlanner 自适应任务规划器 // AdaptivePlanner 自适应任务规划器
type AdaptivePlanner struct { type AdaptivePlanner struct {
registry *RoleRegistry registry *RoleRegistry
assets AssetProvider
} }
// NewAdaptivePlanner 创建自适应任务规划器 // NewAdaptivePlanner 创建自适应任务规划器
func NewAdaptivePlanner(reg *RoleRegistry) *AdaptivePlanner { func NewAdaptivePlanner(reg *RoleRegistry, assets AssetProvider) *AdaptivePlanner {
if reg == nil { if reg == nil {
reg = NewRoleRegistry() reg = NewRoleRegistry()
} }
return &AdaptivePlanner{registry: reg} return &AdaptivePlanner{registry: reg, assets: assets}
} }
// Plan 执行动态规划:区分固定工作流自适应微调 vs 非标文档自主规划 // Plan 执行动态规划:区分固定工作流自适应微调 vs 非标文档自主规划
@@ -157,8 +157,15 @@ func (p *AdaptivePlanner) planFixedAdaptive(req PlannerRequest) (*PlannerResult,
} }
} }
tmplContent, err := template.GetDocTemplate(req.TaskKey) var tmplContent string
if err != nil { 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) tmplContent = fmt.Sprintf("# %s\n\n## 1. 结论\n\n待确认\n\n## 2. 正文\n\n待确认\n", title)
} }
+3 -3
View File
@@ -7,7 +7,7 @@ import (
func TestPlanFixedAdaptive_Default(t *testing.T) { func TestPlanFixedAdaptive_Default(t *testing.T) {
reg := NewRoleRegistry() reg := NewRoleRegistry()
planner := NewAdaptivePlanner(reg) planner := NewAdaptivePlanner(reg, nil)
req := PlannerRequest{ req := PlannerRequest{
TaskKey: "intake", TaskKey: "intake",
@@ -36,7 +36,7 @@ func TestPlanFixedAdaptive_Default(t *testing.T) {
func TestPlanFixedAdaptive_MaterialSnifferInsertStep(t *testing.T) { func TestPlanFixedAdaptive_MaterialSnifferInsertStep(t *testing.T) {
reg := NewRoleRegistry() reg := NewRoleRegistry()
planner := NewAdaptivePlanner(reg) planner := NewAdaptivePlanner(reg, nil)
req := PlannerRequest{ req := PlannerRequest{
TaskKey: "research", TaskKey: "research",
@@ -87,7 +87,7 @@ func TestPlanFixedAdaptive_MaterialSnifferInsertStep(t *testing.T) {
func TestPlanAutonomousAdhoc_CleanroomDesign(t *testing.T) { func TestPlanAutonomousAdhoc_CleanroomDesign(t *testing.T) {
reg := NewRoleRegistry() reg := NewRoleRegistry()
planner := NewAdaptivePlanner(reg) planner := NewAdaptivePlanner(reg, nil)
req := PlannerRequest{ req := PlannerRequest{
TaskKey: "cleanroom_spec", TaskKey: "cleanroom_spec",
+8 -8
View File
@@ -15,7 +15,7 @@ import (
func TestQualityGate_Validate(t *testing.T) { func TestQualityGate_Validate(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: tmpDir, WorkDir: tmpDir,
TaskKey: "intake", TaskKey: "intake",
@@ -88,7 +88,7 @@ func TestQualityGate_Validate(t *testing.T) {
func TestQualityGate_AutoRepair(t *testing.T) { func TestQualityGate_AutoRepair(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: tmpDir, WorkDir: tmpDir,
TaskKey: "intake", TaskKey: "intake",
@@ -135,7 +135,7 @@ func TestQualityGate_AutoRepair(t *testing.T) {
func TestQualityGate_SystemReqsAndTestManager(t *testing.T) { func TestQualityGate_SystemReqsAndTestManager(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
gate := squad.NewQualityGate() gate := squad.NewQualityGate()
// 1. 测试 system_reqs // 1. 测试 system_reqs
@@ -206,7 +206,7 @@ func TestQualityGate_SystemReqsAndTestManager(t *testing.T) {
// 现场证据 output.md:204 — 终稿自检表否定性引用禁词,不得误报为敷衍违规。 // 现场证据 output.md:204 — 终稿自检表否定性引用禁词,不得误报为敷衍违规。
func TestQualityGate_NegatedSelfCheckCitation_NoFalsePositive(t *testing.T) { func TestQualityGate_NegatedSelfCheckCitation_NoFalsePositive(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: tmpDir, WorkDir: tmpDir,
TaskKey: "intake", TaskKey: "intake",
@@ -291,7 +291,7 @@ func TestQualityGate_NegatedSelfCheckCitation_NoFalsePositive(t *testing.T) {
func intakeTemplate(t *testing.T) (workDir string, gate *squad.QualityGate) { func intakeTemplate(t *testing.T) (workDir string, gate *squad.QualityGate) {
t.Helper() t.Helper()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: t.TempDir(), WorkDir: t.TempDir(),
TaskKey: "intake", 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 { if err := os.WriteFile(filepath.Join(workDir, "output.md"), []byte(content), 0o644); err != nil {
t.Fatal(err) 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{ report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &agent.StubBackend{}, 1, squad.ExecuteOptions{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
}) })
@@ -506,7 +506,7 @@ func TestCleanHeadingTitle_MultiDigitChineseOrdinal(t *testing.T) {
// 现场 nightly TestManager:LLM 写「## 十一、签署 / ## 十二、说明」,旧清洗只剥到「十、」会误报缺章。 // 现场 nightly TestManager:LLM 写「## 十一、签署 / ## 十二、说明」,旧清洗只剥到「十、」会误报缺章。
func TestQualityGate_Validate_ChineseOrdinalElevenTwelve(t *testing.T) { func TestQualityGate_Validate_ChineseOrdinalElevenTwelve(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "ws_tm"), WorkDir: filepath.Join(tmpDir, "ws_tm"),
TaskKey: "test_manager", TaskKey: "test_manager",
@@ -552,7 +552,7 @@ func TestNormalizeHeadingTitle_FoldsSpaceWidthAndCase(t *testing.T) {
func TestQualityGate_Validate_HeadingLevelAndSpaceDrift(t *testing.T) { func TestQualityGate_Validate_HeadingLevelAndSpaceDrift(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "ws_research"), WorkDir: filepath.Join(tmpDir, "ws_research"),
TaskKey: "research", TaskKey: "research",
@@ -28,7 +28,7 @@ const repairGoodDraft = `# 用户初始需求收集表
func prepareRepairWS(t *testing.T) string { func prepareRepairWS(t *testing.T) string {
t.Helper() t.Helper()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: t.TempDir(), WorkDir: t.TempDir(),
TaskKey: "intake", 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. // Local repair is promoted only when backend succeeds, parents stay untouched, and the local target is trusted.
func TestEnsureQualityOrRepair_LocalTargetPromotesAndRevalidates(t *testing.T) { func TestEnsureQualityOrRepair_LocalTargetPromotesAndRevalidates(t *testing.T) {
workDir := prepareRepairWS(t) workDir := prepareRepairWS(t)
driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil))
gate := squad.NewQualityGate() gate := squad.NewQualityGate()
report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &localRepairBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &localRepairBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@@ -136,7 +136,7 @@ func TestEnsureQualityOrRepair_LocalTargetPromotesAndRevalidates(t *testing.T) {
func TestEnsureQualityOrRepair_ParentEscapeRestoresAndFailsClosed(t *testing.T) { func TestEnsureQualityOrRepair_ParentEscapeRestoresAndFailsClosed(t *testing.T) {
workDir := prepareRepairWS(t) workDir := prepareRepairWS(t)
orig, _ := os.ReadFile(filepath.Join(workDir, "output.md")) orig, _ := os.ReadFile(filepath.Join(workDir, "output.md"))
driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil))
gate := squad.NewQualityGate() gate := squad.NewQualityGate()
report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &parentEscapeBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &parentEscapeBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@@ -166,7 +166,7 @@ func TestEnsureQualityOrRepair_ParentEscapeRestoresAndFailsClosed(t *testing.T)
func TestEnsureQualityOrRepair_BothChangedFailsClosedNoPromote(t *testing.T) { func TestEnsureQualityOrRepair_BothChangedFailsClosedNoPromote(t *testing.T) {
workDir := prepareRepairWS(t) workDir := prepareRepairWS(t)
orig, _ := os.ReadFile(filepath.Join(workDir, "output.md")) orig, _ := os.ReadFile(filepath.Join(workDir, "output.md"))
driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil))
gate := squad.NewQualityGate() gate := squad.NewQualityGate()
parentDecoy := repairGoodDraft + "\n\n## 越界父级不应被采用\n" parentDecoy := repairGoodDraft + "\n\n## 越界父级不应被采用\n"
report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &bothWriteBackend{ 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) { func TestEnsureQualityOrRepair_RunErrorDoesNotPromoteLocalTarget(t *testing.T) {
workDir := prepareRepairWS(t) workDir := prepareRepairWS(t)
orig, _ := os.ReadFile(filepath.Join(workDir, "output.md")) orig, _ := os.ReadFile(filepath.Join(workDir, "output.md"))
driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil))
gate := squad.NewQualityGate() gate := squad.NewQualityGate()
report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &localThenErrorBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &localThenErrorBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@@ -221,7 +221,7 @@ func TestEnsureQualityOrRepair_CreatedParentInputFailsClosed(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
orig, _ := os.ReadFile(filepath.Join(workDir, "output.md")) orig, _ := os.ReadFile(filepath.Join(workDir, "output.md"))
driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil))
gate := squad.NewQualityGate() gate := squad.NewQualityGate()
report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &createParentInputBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &createParentInputBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@@ -251,7 +251,7 @@ func TestEnsureQualityOrRepair_RestoreFailureStopsWithoutPromote(t *testing.T) {
return fmt.Errorf("restore output.md: injected") return fmt.Errorf("restore output.md: injected")
}) })
defer reset() defer reset()
driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil))
gate := squad.NewQualityGate() gate := squad.NewQualityGate()
report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &bothWriteBackend{ report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &bothWriteBackend{
local: repairGoodDraft, local: repairGoodDraft,
@@ -275,7 +275,7 @@ func TestEnsureQualityOrRepair_RestoreFailureStopsWithoutPromote(t *testing.T) {
func TestEnsureQualityOrRepair_RepairBudgetNotRegressed(t *testing.T) { func TestEnsureQualityOrRepair_RepairBudgetNotRegressed(t *testing.T) {
workDir := prepareRepairWS(t) workDir := prepareRepairWS(t)
driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine()) driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine(nil))
gate := squad.NewQualityGate() gate := squad.NewQualityGate()
_, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &parentEscapeBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{ _, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &parentEscapeBackend{body: repairGoodDraft}, 1, squad.ExecuteOptions{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@@ -290,7 +290,7 @@ func TestEnsureQualityOrRepair_RepairBudgetNotRegressed(t *testing.T) {
} }
func TestAddStep_SnapshotLayoutCopiesNeedsLocally(t *testing.T) { func TestAddStep_SnapshotLayoutCopiesNeedsLocally(t *testing.T) {
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: t.TempDir(), WorkDir: t.TempDir(),
TaskKey: "intake", TaskKey: "intake",
+1 -1
View File
@@ -69,7 +69,7 @@ func TestRDevRegulatoryFollowsComplexityTier(t *testing.T) {
t.Fatal("tasks outside the list stay without regulatory even at high") t.Fatal("tasks outside the list stay without regulatory even at high")
} }
planner := NewAdaptivePlanner(nil) planner := NewAdaptivePlanner(nil, nil)
low, err := planner.Plan(PlannerRequest{ low, err := planner.Plan(PlannerRequest{
TaskKey: "rd_sw_plan", TaskKey: "rd_sw_plan",
Fields: map[string]string{"complexity_tier": "low"}, Fields: map[string]string{"complexity_tier": "low"},
+2 -2
View File
@@ -11,7 +11,7 @@ import (
func TestRDevLeaderPrompt_UsesDesignControlNotBusiness(t *testing.T) { func TestRDevLeaderPrompt_UsesDesignControlNotBusiness(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "rd_design_input_spec"), WorkDir: filepath.Join(tmpDir, "rd_design_input_spec"),
@@ -42,7 +42,7 @@ func TestRDevLeaderPrompt_UsesDesignControlNotBusiness(t *testing.T) {
func TestIntakeLeaderPrompt_StillBusiness(t *testing.T) { func TestIntakeLeaderPrompt_StillBusiness(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "intake"), WorkDir: filepath.Join(tmpDir, "intake"),
+1 -1
View File
@@ -33,7 +33,7 @@ func TestRDevOverlayFiltersRoleAndComplexity(t *testing.T) {
func TestRDevWorkspaceWritesOverlay(t *testing.T) { func TestRDevWorkspaceWritesOverlay(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := NewWorkspaceEngine() engine := NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "rd_planning_review"), WorkDir: filepath.Join(tmpDir, "rd_planning_review"),
TaskKey: "rd_planning_review", TaskKey: "rd_planning_review",
+19 -2
View File
@@ -122,6 +122,23 @@ type Options struct {
// 资产归 Core;Core 只做计算并返回产物,工作区文件由 squad 统一写入)。 // 资产归 Core;Core 只做计算并返回产物,工作区文件由 squad 统一写入)。
// nil 或返回 nil 表示无预处理。 // nil 或返回 nil 表示无预处理。
TaskPreparer func(workDir string, req SquadRunRequest) (*TaskPreparation, error) 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 是领域准备钩子的产物:附件与工作区内回写内容。 // TaskPreparation 是领域准备钩子的产物:附件与工作区内回写内容。
@@ -153,7 +170,7 @@ type Service struct {
} }
func NewService(opts Options) *Service { func NewService(opts Options) *Service {
engine := NewWorkspaceEngine() engine := NewWorkspaceEngine(opts.Assets)
return &Service{ return &Service{
opts: opts.withDefaults(), opts: opts.withDefaults(),
engine: engine, engine: engine,
@@ -215,7 +232,7 @@ func taskDisplayTitle(req SquadRunRequest) string {
// PlanWorkflow 动态规划生成任务工作流 // PlanWorkflow 动态规划生成任务工作流
func (s *Service) PlanWorkflow(ctx context.Context, req SquadRunRequest) (*WorkflowPlan, error) { 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{ res, err := planner.Plan(PlannerRequest{
TaskKey: req.TaskKey, TaskKey: req.TaskKey,
Title: taskDisplayTitle(req), Title: taskDisplayTitle(req),
+1 -1
View File
@@ -100,7 +100,7 @@ func (b *countingBackend) Execute(ctx context.Context, req agent.Request) (*agen
func prepareSingleWriterWS(t *testing.T) (string, *squad.WorkflowDriver) { func prepareSingleWriterWS(t *testing.T) (string, *squad.WorkflowDriver) {
t.Helper() t.Helper()
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: tmpDir, WorkDir: tmpDir,
TaskKey: "prelim_design", TaskKey: "prelim_design",
+1 -1
View File
@@ -55,7 +55,7 @@ type WorkflowDriver struct {
func NewWorkflowDriver(engine *WorkspaceEngine) *WorkflowDriver { func NewWorkflowDriver(engine *WorkspaceEngine) *WorkflowDriver {
if engine == nil { if engine == nil {
engine = NewWorkspaceEngine() engine = NewWorkspaceEngine(nil)
} }
return &WorkflowDriver{engine: engine, evaluator: NewStepEvaluator()} return &WorkflowDriver{engine: engine, evaluator: NewStepEvaluator()}
} }
+1 -1
View File
@@ -15,7 +15,7 @@ import (
func TestWorkflowDriver_DynamicAndExecute(t *testing.T) { func TestWorkflowDriver_DynamicAndExecute(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{ ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
WorkDir: tmpDir, WorkDir: tmpDir,
TaskKey: "intake", TaskKey: "intake",
+18 -9
View File
@@ -7,7 +7,6 @@ import (
"strings" "strings"
"time" "time"
"chat-agent-system/internal/template"
) )
// WorkspacePrepareRequest 工作区准备请求 // WorkspacePrepareRequest 工作区准备请求
@@ -60,10 +59,12 @@ type Workspace struct {
} }
// WorkspaceEngine 工作区生命周期与沙箱构建引擎 // WorkspaceEngine 工作区生命周期与沙箱构建引擎
type WorkspaceEngine struct{} type WorkspaceEngine struct {
assets AssetProvider
}
func NewWorkspaceEngine() *WorkspaceEngine { func NewWorkspaceEngine(assets AssetProvider) *WorkspaceEngine {
return &WorkspaceEngine{} return &WorkspaceEngine{assets: assets}
} }
// PrepareWorkspace 初始化自包含工作区目录 // PrepareWorkspace 初始化自包含工作区目录
@@ -136,7 +137,7 @@ func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Worksp
Steps: steps, Steps: steps,
} }
} else { } else {
planner := NewAdaptivePlanner(nil) planner := NewAdaptivePlanner(nil, e.assets)
pRes, pErr := planner.Plan(PlannerRequest{ pRes, pErr := planner.Plan(PlannerRequest{
TaskKey: req.TaskKey, TaskKey: req.TaskKey,
Title: req.StageTitle, Title: req.StageTitle,
@@ -187,13 +188,16 @@ func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Worksp
} }
// 4. 写入自描述模板 template.md // 4. 写入自描述模板 template.md
if strings.TrimSpace(tmplContent) == "" { if strings.TrimSpace(tmplContent) == "" && e.assets != nil {
var err error var err error
tmplContent, err = template.GetDocTemplate(req.TaskKey) tmplContent, err = e.assets.DocTemplate(req.TaskKey)
if err != nil { 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{ vars := map[string]string{
"project_name": req.ProjectName, "project_name": req.ProjectName,
"product_name": req.ProjectName, "product_name": req.ProjectName,
@@ -207,7 +211,12 @@ func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Worksp
for k, v := range req.InitialFields { for k, v := range req.InitialFields {
vars[k] = v 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 { if err := os.WriteFile(filepath.Join(workDir, "template.md"), []byte(renderedTmpl), 0o644); err != nil {
return nil, fmt.Errorf("write template.md: %w", err) return nil, fmt.Errorf("write template.md: %w", err)
} }
+7 -6
View File
@@ -6,7 +6,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"chat-agent-system/internal/template"
) )
// buildSubWorkspaces 为 workflow 中的各个子任务构建独立沙箱 // buildSubWorkspaces 为 workflow 中的各个子任务构建独立沙箱
@@ -27,11 +26,13 @@ func (e *WorkspaceEngine) buildSubWorkspaces(workDir string, wf WorkflowPlan, sq
spec := agentMap[step.Role] spec := agentMap[step.Role]
var inst string var inst string
var err error var err error
if file := strings.TrimSpace(spec.InstructionsFile); file != "" { if e.assets != nil {
inst, err = template.GetAgentInstructionFile(file) if file := strings.TrimSpace(spec.InstructionsFile); file != "" {
} inst, err = e.assets.AgentInstructionFile(file)
if err != nil || strings.TrimSpace(inst) == "" { }
inst, err = template.GetAgentInstruction(step.Role) if err != nil || strings.TrimSpace(inst) == "" {
inst, err = e.assets.AgentInstruction(step.Role)
}
} }
if err == nil && strings.TrimSpace(inst) != "" { if err == nil && strings.TrimSpace(inst) != "" {
agentsBuilder.WriteString(inst) agentsBuilder.WriteString(inst)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
func TestWorkspaceEngine_PrepareWorkspace(t *testing.T) { func TestWorkspaceEngine_PrepareWorkspace(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
engine := squad.NewWorkspaceEngine() engine := squad.NewWorkspaceEngine(nil)
req := squad.WorkspacePrepareRequest{ req := squad.WorkspacePrepareRequest{
WorkDir: filepath.Join(tmpDir, "test_workspace_01"), WorkDir: filepath.Join(tmpDir, "test_workspace_01"),