// gate.go 实现阶段网关:把"agent 停了"(stopReason)与"工作确实就绪"(产物/issue // 谓词)结合,避免仅凭 end_turn 就静默把需求标记完成。v1 是可插拔的 per-phase 谓词。 package orchestrator import ( "context" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/project" ) // GateArtifactAccess 是网关查询某需求阶段产物的窄接口(app 层用 project.ArtifactService 适配)。 type GateArtifactAccess interface { // ListArtifacts 返回某 requirement 在某 phase 的产物(phase 传零值 "" 表示全部阶段)。 ListArtifacts(ctx context.Context, projectID, requirementID uuid.UUID, phase project.Phase) ([]*project.Artifact, error) } // GateIssueAccess 是网关查询某需求未关闭 issue 数的窄接口(app 层用 project.IssueService/Repository 适配)。 type GateIssueAccess interface { CountOpenIssues(ctx context.Context, requirementID uuid.UUID) (int, error) } // v1Gate 是 v1 阶段网关: // // - stopReason 必须是 end_turn,否则不通过。refusal/max_tokens 等明确是 agent // 主动放弃/受限,不通过且不重试(Retry=false → dead-letter)。其它非空 stopReason // 不通过但可重试。 // - 产物型阶段(planning/prototyping/auditing/implementing/reviewing)额外要求该阶段 // 已有至少一个产物;缺失则不通过但可重试(让 agent 再跑一次产出)。 // - reviewing/done 额外检查未关闭 issue 数为 0(有开放 issue 视为未就绪,可重试)。 // - done 阶段:end_turn 即视为通过(汇总视图,无单独产物)。 type v1Gate struct { artifacts GateArtifactAccess // 可为 nil(跳过产物谓词) issues GateIssueAccess // 可为 nil(跳过 issue 谓词) } // NewV1Gate 构造 v1 阶段网关。artifacts/issues 可为 nil(对应谓词被跳过)。 func NewV1Gate(artifacts GateArtifactAccess, issues GateIssueAccess) PhaseGate { return &v1Gate{artifacts: artifacts, issues: issues} } func (g *v1Gate) Evaluate(ctx context.Context, run *Run, step *Step, stopReason string) (GateResult, error) { // 1. stopReason 闸门。 switch stopReason { case string(acpStopEndTurn): // 继续 phase 谓词。 case string(acpStopRefusal), string(acpStopMaxTokens): return GateResult{Pass: false, Retry: false, Reason: "agent stopped with non-recoverable reason: " + stopReason}, nil default: // 空串 / cancelled / vendor-specific:不通过,可重试。 reason := stopReason if reason == "" { reason = "no stop reason" } return GateResult{Pass: false, Retry: true, Reason: "turn did not end cleanly: " + reason}, nil } phase := step.Phase // 2. done:end_turn 即通过。 if phase == project.PhaseDone { return GateResult{Pass: true, Reason: "done"}, nil } // 3. 产物型阶段:要求该阶段已有产物。 if phase.IsArtifactPhase() && g.artifacts != nil { arts, err := g.artifacts.ListArtifacts(ctx, run.ProjectID, run.RequirementID, phase) if err != nil { return GateResult{}, err } if len(arts) == 0 { return GateResult{Pass: false, Retry: true, Reason: "no artifact produced for phase " + string(phase), Detail: map[string]any{"phase": string(phase)}}, nil } } // 4. reviewing:额外要求无开放 issue(避免遗留缺陷推进)。 if phase == project.PhaseReviewing && g.issues != nil { open, err := g.issues.CountOpenIssues(ctx, run.RequirementID) if err != nil { return GateResult{}, err } if open > 0 { return GateResult{Pass: false, Retry: true, Reason: "open issues remain", Detail: map[string]any{"open_issues": open}}, nil } } return GateResult{Pass: true, Reason: "phase gate passed", Detail: map[string]any{"phase": string(phase)}}, nil } // acp stopReason 原始字符串常量(与 internal/acp.StopReason 取值一致,此处复制以免引入 // acp 依赖到纯网关逻辑)。 const ( acpStopEndTurn = "end_turn" acpStopRefusal = "refusal" acpStopMaxTokens = "max_tokens" )