Files
weichen b6ff0ab55f
ci / vet-and-test (push) Failing after 2s
ci / docker-checkout-hostgateway (push) Failing after 2s
Add catalog-driven orchestration: intent catalog injection
Details (original message):
Add catalog-driven orchestration: intent catalog injection, deterministic plan validation, and system-function steps.
2026-09-29 16:37:46 +00:00

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]) + "…"
}