You've already forked agentic-coding-workflow
164 lines
5.7 KiB
Go
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
|
|
}
|