You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user