// planner.go 是纯函数(无 I/O)的阶段规划逻辑:阶段→agent-kind 解析、阶段→prompt // 组装、网关结果→下一阶段。可独立单测(模仿 acp.ResolveBranch 的取舍)。 package orchestrator import ( "fmt" "strings" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" "github.com/yan1h/agent-coding-workflow/internal/project" ) // phaseOrder 是 6 阶段的线性推进顺序,对应 project.AllPhases。 var phaseOrder = project.AllPhases // NextPhase 根据当前阶段与网关结果决定下一阶段。 // // - 网关未通过(!g.Pass)→ 回环本阶段(next==current,terminal=false),让 step // 在同阶段重试。 // - 网关通过且 current==done → 终态(terminal=true),run 成功。 // - 网关通过且 current!=done → 线性推进到下一阶段。 // // current 非法(不在枚举内)时视为终态,避免无限循环。 func NextPhase(current project.Phase, g GateResult) (next project.Phase, terminal bool) { if !g.Pass { return current, false } if current == project.PhaseDone { return project.PhaseDone, true } for i, p := range phaseOrder { if p == current { if i+1 >= len(phaseOrder) { // current 已是最后一个阶段(done)——由上面的分支处理;兜底终止。 return project.PhaseDone, true } // 推进到下一阶段。即便下一阶段是 done,也非终态:done 仍要跑一个 step, // 只有 done 阶段网关通过后(current==done 分支)run 才成功。 return phaseOrder[i+1], false } } // 未知阶段:保守终止。 return current, true } // PhaseAgentKind 解析某阶段应使用的 agent-kind:优先 run.config 覆盖,否则退回 // defaults(app 层从配置注入的"阶段→默认 agent-kind"映射)。两者皆缺则报错。 func PhaseAgentKind(cfg RunConfig, phase project.Phase, defaults map[project.Phase]uuid.UUID) (uuid.UUID, error) { if cfg.PhaseAgentKinds != nil { if id, ok := cfg.PhaseAgentKinds[phase]; ok && id != uuid.Nil { return id, nil } } if defaults != nil { if id, ok := defaults[phase]; ok && id != uuid.Nil { return id, nil } } return uuid.Nil, errs.New(errs.CodeInvalidInput, fmt.Sprintf("no agent kind configured for phase %q", phase)) } // BuildPrompt 组装某阶段发给 agent 的 prompt。config 提供覆盖模板时优先用覆盖 // (仍前置需求/产物上下文);否则用内置的 per-phase 默认指令。纯字符串拼接,无 I/O。 func BuildPrompt(phase project.Phase, req *project.Requirement, artifacts []*project.Artifact, override string) string { var b strings.Builder if req != nil { fmt.Fprintf(&b, "Requirement #%d: %s\n", req.Number, req.Title) if strings.TrimSpace(req.Description) != "" { fmt.Fprintf(&b, "\nDescription:\n%s\n", strings.TrimSpace(req.Description)) } } if len(artifacts) > 0 { b.WriteString("\nPrior artifacts:\n") for _, a := range artifacts { if a == nil { continue } fmt.Fprintf(&b, "- [%s v%d] %s\n", a.Phase, a.Version, summarizeArtifact(a)) } } b.WriteString("\nCurrent phase: ") b.WriteString(string(phase)) b.WriteString("\n\n") if strings.TrimSpace(override) != "" { b.WriteString(strings.TrimSpace(override)) b.WriteString("\n") return b.String() } b.WriteString(defaultPhaseInstruction(phase)) b.WriteString("\n") return b.String() } // summarizeArtifact 取产物内容首行(截断)作为简介,避免 prompt 过长。 func summarizeArtifact(a *project.Artifact) string { content := strings.TrimSpace(a.Content) if content == "" { return "(empty)" } if idx := strings.IndexByte(content, '\n'); idx >= 0 { content = content[:idx] } if len(content) > 120 { content = content[:120] + "..." } return content } // defaultPhaseInstruction 是每个阶段的内置默认指令。 func defaultPhaseInstruction(phase project.Phase) string { switch phase { case project.PhasePlanning: return "Produce a clear plan for this requirement and record it as a planning artifact." case project.PhasePrototyping: return "Build a prototype validating the approach and record a prototyping artifact." case project.PhaseAuditing: return "Audit the plan/prototype for risks and gaps; record an auditing artifact." case project.PhaseImplementing: return "Implement the requirement on this worktree; commit your changes." case project.PhaseReviewing: return "Review the implementation, fix issues, and record a reviewing artifact." case project.PhaseDone: return "Summarize the completed work; no further action is required." default: return "Proceed with the current phase." } }