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

164 lines
5.7 KiB
Go

// decomposition.go 实现 ApplyDecomposition:让规划阶段的 agent 在一次 MCP 调用中
// 原子地提交整张任务分解图(子任务 + blocks/blocked-by 边 + 优先级),而不必发 N 次
// create_subtask + M 次 add_dependency。先在内存中对 in-batch 图做 DAG 校验(拓扑排序),
// 全部通过后再落库:先建全部子任务,再建全部依赖边。校验失败则在任何写入前返回,
// 不留半成品。
package orchestrator
import (
"context"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
"github.com/yan1h/agent-coding-workflow/internal/project"
)
// SubtaskSpec 描述分解图里的一个子任务。Ref 是 batch 内的本地引用键(如 "a"/"1"),
// 供 EdgeSpec 跨条目引用;落库后映射为真实 issue number。
type SubtaskSpec struct {
Ref string
Title string
Description string
Priority int
}
// EdgeSpec 描述一条依赖边:BlockedRef 被 BlockerRef 阻塞。两端均为 batch 内 ref。
type EdgeSpec struct {
BlockedRef string
BlockerRef string
}
// Decomposition 是一次完整任务分解提交。
type Decomposition struct {
// ParentNumber 非 nil 时所有子任务挂在该 issue 下(通常是 requirement 的锚定 issue)。
ParentNumber *int
Subtasks []SubtaskSpec
Edges []EdgeSpec
}
// DecompositionResult 报告落库结果:ref → 真实 issue number。
type DecompositionResult struct {
// RefToNumber 把每个 SubtaskSpec.Ref 映射到创建出的 issue number。
RefToNumber map[string]int
}
// DecompositionApplier 是 ApplyDecomposition 依赖的窄 issue 服务接口(project.IssueService 满足)。
type DecompositionApplier interface {
CreateSubtask(ctx context.Context, c project.Caller, projectSlug string, parentNumber int, in project.CreateIssueInput) (*project.Issue, error)
Create(ctx context.Context, c project.Caller, projectSlug string, in project.CreateIssueInput) (*project.Issue, error)
AddDependency(ctx context.Context, c project.Caller, projectSlug string, in project.AddDependencyInput) (*project.Dependency, error)
}
// ApplyDecomposition 原子(best-effort)地落库一张任务分解图。先校验 in-batch 图是 DAG,
// 再建子任务、建边。返回 ref→number 映射。校验在任何写入前完成,故无效图不留半成品。
func ApplyDecomposition(ctx context.Context, svc DecompositionApplier, c project.Caller, projectSlug string, d Decomposition) (DecompositionResult, error) {
res := DecompositionResult{RefToNumber: map[string]int{}}
if len(d.Subtasks) == 0 {
return res, errs.New(errs.CodeInvalidInput, "decomposition 至少需要一个子任务")
}
// 1) ref 去重 + 建集合。
refSet := make(map[string]struct{}, len(d.Subtasks))
for _, st := range d.Subtasks {
if st.Ref == "" {
return res, errs.New(errs.CodeInvalidInput, "子任务 ref 不能为空")
}
if st.Title == "" {
return res, errs.New(errs.CodeInvalidInput, "子任务 title 不能为空")
}
if _, dup := refSet[st.Ref]; dup {
return res, errs.New(errs.CodeInvalidInput, "子任务 ref 重复: "+st.Ref)
}
if st.Priority < 0 || st.Priority > 3 {
return res, errs.New(errs.CodeInvalidInput, "子任务 priority 必须在 0..3")
}
refSet[st.Ref] = struct{}{}
}
// 2) 校验边端点存在、非自环。
for _, e := range d.Edges {
if _, ok := refSet[e.BlockedRef]; !ok {
return res, errs.New(errs.CodeInvalidInput, "边引用了未知 blocked ref: "+e.BlockedRef)
}
if _, ok := refSet[e.BlockerRef]; !ok {
return res, errs.New(errs.CodeInvalidInput, "边引用了未知 blocker ref: "+e.BlockerRef)
}
if e.BlockedRef == e.BlockerRef {
return res, errs.New(errs.CodeInvalidInput, "边不能自指: "+e.BlockedRef)
}
}
// 3) DAG 校验:沿 blocked-by 边(blocked → blocker)做拓扑排序,存在环则拒绝。
if err := validateDAG(d.Subtasks, d.Edges); err != nil {
return res, err
}
// 4) 落库:先建全部子任务(记录 ref→number),再建全部依赖边。
for _, st := range d.Subtasks {
prio := st.Priority
in := project.CreateIssueInput{
Title: st.Title,
Description: st.Description,
Priority: &prio,
}
var iss *project.Issue
var err error
if d.ParentNumber != nil {
iss, err = svc.CreateSubtask(ctx, c, projectSlug, *d.ParentNumber, in)
} else {
iss, err = svc.Create(ctx, c, projectSlug, in)
}
if err != nil {
return res, err
}
res.RefToNumber[st.Ref] = iss.Number
}
for _, e := range d.Edges {
if _, err := svc.AddDependency(ctx, c, projectSlug, project.AddDependencyInput{
BlockedNumber: res.RefToNumber[e.BlockedRef],
BlockerNumber: res.RefToNumber[e.BlockerRef],
}); err != nil {
return res, err
}
}
return res, nil
}
// validateDAG 对 (子任务, 边) 做 Kahn 拓扑排序,存在环返回 CodeInvalidInput。
// 边语义 blocked blocked-by blocker,建图方向 blocker → blocked(依赖先于被依赖)。
func validateDAG(subtasks []SubtaskSpec, edges []EdgeSpec) error {
indeg := make(map[string]int, len(subtasks))
for _, st := range subtasks {
indeg[st.Ref] = 0
}
adj := make(map[string][]string, len(subtasks))
// blocker → blocked:blocked 的入度 +1。
for _, e := range edges {
adj[e.BlockerRef] = append(adj[e.BlockerRef], e.BlockedRef)
indeg[e.BlockedRef]++
}
queue := make([]string, 0, len(subtasks))
for ref, d := range indeg {
if d == 0 {
queue = append(queue, ref)
}
}
visited := 0
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
visited++
for _, next := range adj[cur] {
indeg[next]--
if indeg[next] == 0 {
queue = append(queue, next)
}
}
}
if visited != len(subtasks) {
return errs.New(errs.CodeInvalidInput, "decomposition 边集合存在环")
}
return nil
}