Details (original message): Add catalog-driven orchestration: intent catalog injection, deterministic plan validation, and system-function steps.
181 lines
5.2 KiB
Go
181 lines
5.2 KiB
Go
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]) + "…"
|
|
}
|