package squad import ( "context" "fmt" "strings" ) // RulePlanner 规则规划器(零模型依赖): // - 命中蓝图:直接采用基线步骤/编制; // - 未命中:若注册表为空则澄清;否则按通用四阶段骨架(梳理→专项→成文→终审) // 组建小队:优先推荐匹配角色,不足时派生特设角色。 type RulePlanner struct{} // NewRulePlanner 创建规则规划器。 func NewRulePlanner() *RulePlanner { return &RulePlanner{} } func (p *RulePlanner) Name() string { return "rule" } func (p *RulePlanner) Plan(ctx context.Context, input PlanInput) (*PlanOutput, error) { if input.Blueprint != nil { return p.fromBlueprint(input) } return p.autonomous(input) } func (p *RulePlanner) fromBlueprint(input PlanInput) (*PlanOutput, error) { bp := *input.Blueprint steps := make([]StepSpec, len(bp.Steps)) copy(steps, bp.Steps) plan := &SquadPlan{ Squad: SquadConfig{ SquadName: bp.Title, Version: "1", Agents: append([]RoleSpec{}, bp.Agents...), }, Workflow: WorkflowPlan{ WorkflowID: NewPlanID(), TaskKey: bp.Key, Title: bp.Title, PlanningMode: PlanModeBlueprint, Status: "pending", Steps: steps, }, Template: bp.Template, } // 蓝图内联角色并入注册表(引用已注册角色的声明以注册表为准)。 for _, a := range bp.Agents { if _, exists := input.Registry.Get(a.Role); !exists { if err := input.Registry.Register(a); err != nil { return nil, err } } } return &PlanOutput{Plan: plan}, nil } func (p *RulePlanner) autonomous(input PlanInput) (*PlanOutput, error) { goalText := strings.TrimSpace(input.Goal.Text) if goalText == "" { return nil, fmt.Errorf("goal text is empty") } roles := input.Registry.List() if len(roles) == 0 { // 空注册表无法组建小队:请求澄清(提示先注册角色)。 return &PlanOutput{Clarification: &Clarification{ Reason: "角色注册表为空:请先注册角色,或提供蓝图模板", Questions: []Question{{ ID: "q_roles", Text: "当前没有任何已注册角色。是否以内置最小编制(planner/writer/reviewer)执行该目标?", Options: []HumanOption{ {Value: "minimal", Label: "使用最小编制(自动派生 3 个通用角色)"}, {Value: "abort", Label: "中止,我先注册角色"}, }, }}, }}, nil } // 通用四阶段骨架:梳理 → 专项 → 成文 → 终审(角色键与 minimal 引导一致)。 stages := []struct { id string title string role string }{ {"step_01_organizer", "梳理目标背景与输入材料,产出事实与缺口", "organizer"}, {"step_02_specialist", "围绕目标开展专业分析与推演", "specialist"}, {"step_03_writer", "汇总各角色产出,撰写交付物初稿", "writer"}, {"step_04_leader", "一致性终审与交付把关", "leader"}, } var agents []RoleSpec var steps []StepSpec usedFallback := false for i, st := range stages { spec, ok := p.pickRole(input.Registry, goalText, st.id) if !ok { // 以推荐角色/首个角色为基线派生通用角色。 base := p.baseRole(input.Registry) derived, err := input.Registry.Derive(DeriveRequest{ RoleKey: st.role, Name: fmt.Sprintf("%s专员", st.role), BaseRole: base.Role, DomainTopic: goalText, Responsibilities: []string{st.title}, }) if err != nil { return nil, err } spec = derived usedFallback = true } if _, seen := findAgent(agents, spec.Role); !seen { agents = append(agents, spec) } targets := []string{"output.md"} if i < len(stages)-1 { targets = []string{"notes.md", "fields.json"} } steps = append(steps, StepSpec{ ID: st.id, Role: spec.Role, Title: st.title, Targets: targets, Status: "pending", }) } plan := &SquadPlan{ Squad: SquadConfig{SquadName: truncate(goalText, 40), Version: "1", Agents: agents}, Workflow: WorkflowPlan{ WorkflowID: NewPlanID(), Title: truncate(goalText, 80), PlanningMode: PlanModeAutonomous, Status: "pending", Steps: steps, }, } out := &PlanOutput{Plan: plan} if usedFallback { out.NeedsConfirmation = true out.ConfirmReason = "注册表中缺少对应角色,计划包含现场派生的通用角色,请确认编制" } return out, nil } // pickRole 为阶段挑选角色:organizer/leader 按同名词或推荐,specialist 按推荐,writer 同名或推荐。 func (p *RulePlanner) pickRole(reg *RoleRegistry, goalText, stageID string) (RoleSpec, bool) { if direct, ok := reg.Get(strings.SplitN(stageID, "_", 3)[2]); ok { return direct, true } return reg.Recommend(goalText) } func (p *RulePlanner) baseRole(reg *RoleRegistry) RoleSpec { if rec, ok := reg.Recommend(""); ok { return rec } list := reg.List() if len(list) > 0 { return list[0] } return RoleSpec{Role: "base", Name: "base", BashPermission: "deny"} } func findAgent(agents []RoleSpec, role string) (RoleSpec, bool) { for _, a := range agents { if a.Role == role { return a, true } } return RoleSpec{}, false } func truncate(s string, n int) string { r := []rune(s) if len(r) <= n { return s } return string(r[:n]) + "…" }