Files
2026-06-22 08:55:57 +08:00

136 lines
4.6 KiB
Go

// 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."
}
}