You've already forked agentic-coding-workflow
105 lines
4.0 KiB
Go
105 lines
4.0 KiB
Go
// 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"
|
|
)
|