// step_handler.go 把 orchestrator.step 接入 jobs:每个 step 由一个 orchestrator.step // job 驱动,因而能跨重启恢复并复用既有 backoff/dead-letter 机制。 package orchestrator import ( "context" "encoding/json" "fmt" "log/slog" "time" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" "github.com/yan1h/agent-coding-workflow/internal/jobs" "github.com/yan1h/agent-coding-workflow/internal/project" ) // JobTypeOrchestratorStep 是驱动单个编排 step 的 job 类型。 const JobTypeOrchestratorStep jobs.JobType = "orchestrator.step" // stepPayload 是 orchestrator.step job 的 payload。 type stepPayload struct { RunID uuid.UUID `json:"run_id"` StepID uuid.UUID `json:"step_id"` } // StepHandlerDeps 是 StepHandler 的依赖集合。 type StepHandlerDeps struct { Repo Repository Runner *runner Gate PhaseGate PM PMAccess Enqueue StepEnqueuer DefaultAgentKind map[project.Phase]uuid.UUID Config Config Audit AuditRecorder Log *slog.Logger } // StepHandler 实现 jobs.Handler,驱动一个阶段回合并推进/回环状态机。 type StepHandler struct { repo Repository runner *runner gate PhaseGate pm PMAccess enqueue StepEnqueuer defaults map[project.Phase]uuid.UUID cfg Config audit AuditRecorder log *slog.Logger } // NewStepHandler 构造 StepHandler。 func NewStepHandler(d StepHandlerDeps) *StepHandler { log := d.Log if log == nil { log = slog.Default() } return &StepHandler{ repo: d.Repo, runner: d.Runner, gate: d.Gate, pm: d.PM, enqueue: d.Enqueue, defaults: d.DefaultAgentKind, cfg: d.Config, audit: d.Audit, log: log, } } // Type 返回 job 类型。 func (h *StepHandler) Type() jobs.JobType { return JobTypeOrchestratorStep } // Handle 驱动一个 step:load step+run;run 非活跃 → no-op;spawn-or-resume 跑回合; // 评估网关;通过 → ChangePhase + 入队下一 step(或 done → run 成功);否则递增 attempt // 并返回 error(attempt 耗尽 → jobs.Permanent → dead-letter)。 // // 幂等:先按 step.status 短路,避免重复 leased 时重复 spawn(worker 已有 Complete fence)。 func (h *StepHandler) Handle(ctx context.Context, job jobs.Job) error { var p stepPayload if err := json.Unmarshal(job.Payload, &p); err != nil { return jobs.Permanent(errs.Wrap(err, errs.CodeInvalidInput, "orchestrator.step: bad payload")) } step, err := h.repo.GetStep(ctx, p.StepID) if err != nil { // step 不存在:永久失败(无法恢复)。 return jobs.Permanent(err) } // 已到终态:no-op(重复投递 / 重启重入)。 switch step.Status { case StepSucceeded, StepFailed, StepDead: return nil } run, err := h.repo.GetRun(ctx, step.RunID) if err != nil { return jobs.Permanent(err) } // run 已暂停/取消/终态:不驱动,完成 job(释放 worker)。 if !run.Status.IsActive() { h.log.Info("orchestrator.step.skip_inactive_run", "run_id", run.ID, "step_id", step.ID, "run_status", run.Status) return nil } // run.status pending → running(首个 step 起跑)。 if run.Status == RunPending { if _, uerr := h.repo.UpdateRunStatus(ctx, run.ID, RunRunning, nil); uerr != nil { h.log.Warn("orchestrator.run.mark_running_failed", "run_id", run.ID, "err", uerr.Error()) } } // 解析需求(用于 prompt 上下文 + 阶段绑定 + ChangePhase)。 req, err := h.pm.GetRequirementByID(ctx, run.RequirementID) if err != nil { return jobs.Permanent(err) } proj, err := h.pm.GetProjectByID(ctx, run.ProjectID) if err != nil { return jobs.Permanent(err) } // step.prompt 为空时(首建只填了占位)补建 prompt。通常 service 建 step 时已填。 if step.Prompt == "" { artifacts, _ := h.pm.ListLatestArtifacts(ctx, run.RequirementID) override := run.Config.PromptOverrides[step.Phase] step.Prompt = BuildPrompt(step.Phase, req, artifacts, override) } if step.Status == StepAwaitingGate && step.GateResult != nil && step.GateResult.Pass { return h.advancePassedStep(ctx, run, step, req, proj, *step.GateResult, "") } // 标记 spawning(关联本 job)。 jobID := job.ID step.Status = StepSpawning step.JobID = &jobID if updated, uerr := h.repo.UpdateStepStatus(ctx, step); uerr == nil { step = updated } // 驱动回合(spawn-or-resume + SendPromptAndWait)。 res, terr := h.runner.driveTurn(ctx, run, step, req.Number) if terr != nil { return h.failStep(ctx, run, step, terr) } // 记录 session + stopReason,进入 awaiting_gate。 step.ACPSessionID = &res.SessionID stop := res.StopReason step.StopReason = &stop step.Status = StepAwaitingGate if updated, uerr := h.repo.UpdateStepStatus(ctx, step); uerr == nil { step = updated } // 评估网关。 gate, gerr := h.gate.Evaluate(ctx, run, step, res.StopReason) if gerr != nil { return h.failStep(ctx, run, step, gerr) } step.GateResult = &gate if !gate.Pass { return h.failStepGate(ctx, run, step, gate) } // 网关通过后先停在 awaiting_gate;只有 project.ChangePhase 与 run phase 都写成功后, // 才能把 step 标为 succeeded 并入队下一阶段,避免绕过 project 的 entry gate。 step.Status = StepAwaitingGate step.GateResult = &gate step.LastError = nil if updated, uerr := h.repo.UpdateStepStatus(ctx, step); uerr == nil { step = updated } return h.advancePassedStep(ctx, run, step, req, proj, gate, res.StopReason) } func (h *StepHandler) advancePassedStep(ctx context.Context, run *Run, step *Step, req *project.Requirement, proj *project.Project, gate GateResult, stopReason string) error { // 推进阶段。 next, terminal := NextPhase(step.Phase, gate) if terminal { step.Status = StepSucceeded step.LastError = nil if updated, uerr := h.repo.UpdateStepStatus(ctx, step); uerr == nil { step = updated } h.recordAudit(ctx, run, "orchestrator.step.advance", step.ID.String(), map[string]any{ "phase": string(step.Phase), "stop_reason": stopReason, }) // 整条 run 完成。 if _, uerr := h.repo.UpdateRunStatus(ctx, run.ID, RunSucceeded, nil); uerr != nil { h.log.Warn("orchestrator.run.mark_succeeded_failed", "run_id", run.ID, "err", uerr.Error()) } h.recordAudit(ctx, run, "orchestrator.run.succeeded", run.ID.String(), nil) return nil } // 写需求阶段(run.owner 身份)。失败必须阻塞编排推进;project.ChangePhase 是 // 更靠近领域状态的准入网关,不能被 orchestrator.run.current_phase 越过。 if cerr := h.pm.ChangeRequirementPhase(ctx, run.OwnerID, false, proj.Slug, req.Number, next); cerr != nil { h.log.Warn("orchestrator.change_phase_failed", "run_id", run.ID, "to", next, "err", cerr.Error()) return h.failPhaseAdvance(ctx, run, step, next, cerr) } if _, uerr := h.repo.UpdateRunPhase(ctx, run.ID, next); uerr != nil { h.log.Warn("orchestrator.run.update_phase_failed", "run_id", run.ID, "err", uerr.Error()) return h.failPhaseAdvance(ctx, run, step, next, uerr) } step.Status = StepSucceeded step.LastError = nil if updated, uerr := h.repo.UpdateStepStatus(ctx, step); uerr == nil { step = updated } h.recordAudit(ctx, run, "orchestrator.step.advance", step.ID.String(), map[string]any{ "phase": string(step.Phase), "stop_reason": stopReason, }) // 建并入队下一阶段 step。 if _, eerr := h.enqueueNextStep(ctx, run, next); eerr != nil { return eerr } return nil } // enqueueNextStep 为 run 在 phase 上建一个新 step 并入队其 job。复用 service 的逻辑。 func (h *StepHandler) enqueueNextStep(ctx context.Context, run *Run, phase project.Phase) (*Step, error) { step, err := buildPhaseStep(ctx, h.repo, h.pm, run, phase, nil, 0, h.defaults) if err != nil { // 无法为下一阶段建 step(如缺 agent kind):标 run 失败。 msg := err.Error() if _, uerr := h.repo.UpdateRunStatus(ctx, run.ID, RunFailed, &msg); uerr != nil { h.log.Warn("orchestrator.run.mark_failed", "run_id", run.ID, "err", uerr.Error()) } return nil, jobs.Permanent(err) } if eerr := h.enqueue.EnqueueStep(ctx, run.ID, step.ID, time.Now().UTC()); eerr != nil { return nil, eerr } return step, nil } // failStep 处理回合驱动/网关评估的错误:递增 attempt,返回可重试或 Permanent error。 func (h *StepHandler) failStep(ctx context.Context, run *Run, step *Step, cause error) error { msg := cause.Error() step.Status = StepRunning // 保持非终态,等待重试或 dead-letter 落定 step.LastError = &msg _, _ = h.repo.UpdateStepStatus(ctx, step) updated, ierr := h.repo.IncrementStepAttempt(ctx, step.ID) if ierr == nil { step = updated } return h.retryOrDead(ctx, run, step, cause, false) } // failStepGate 处理网关未通过:Retry=false → 立即 Permanent;否则递增 attempt 后判断。 func (h *StepHandler) failStepGate(ctx context.Context, run *Run, step *Step, gate GateResult) error { reason := gate.Reason step.LastError = &reason step.Status = StepRunning _, _ = h.repo.UpdateStepStatus(ctx, step) cause := errs.New(errs.CodePhaseGateFailed, gate.Reason) if !gate.Retry { return h.markDead(ctx, run, step, cause) } updated, ierr := h.repo.IncrementStepAttempt(ctx, step.ID) if ierr == nil { step = updated } return h.retryOrDead(ctx, run, step, cause, true) } func (h *StepHandler) failPhaseAdvance(ctx context.Context, run *Run, step *Step, next project.Phase, cause error) error { msg := cause.Error() step.Status = StepAwaitingGate step.LastError = &msg _, _ = h.repo.UpdateStepStatus(ctx, step) if ae, ok := errs.As(cause); ok && ae.Code == errs.CodePhaseGateFailed { if _, uerr := h.repo.UpdateRunStatus(ctx, run.ID, RunPaused, &msg); uerr != nil { h.log.Warn("orchestrator.run.mark_paused_failed", "run_id", run.ID, "err", uerr.Error()) } h.recordAudit(ctx, run, "orchestrator.step.phase_blocked", step.ID.String(), map[string]any{ "phase": string(step.Phase), "next": string(next), "err": msg, }) return nil } updated, ierr := h.repo.IncrementStepAttempt(ctx, step.ID) if ierr == nil { step = updated } return h.retryOrDead(ctx, run, step, cause, false) } // retryOrDead 根据 attempt 与 MaxAttemptsPerPhase 决定可重试还是 dead-letter。 func (h *StepHandler) retryOrDead(ctx context.Context, run *Run, step *Step, cause error, gateFail bool) error { maxAttempts := run.Config.MaxAttemptsPerPhase if maxAttempts <= 0 { maxAttempts = h.cfg.MaxAttemptsPerPhase } if maxAttempts <= 0 { maxAttempts = 3 } if step.Attempt >= maxAttempts { return h.markDead(ctx, run, step, cause) } h.recordAudit(ctx, run, "orchestrator.step.retry", step.ID.String(), map[string]any{ "phase": string(step.Phase), "attempt": step.Attempt, "err": cause.Error(), "gate": gateFail, }) // 返回非 Permanent error:jobs worker 走 backoff 重试,重新驱动本 step。 return fmt.Errorf("orchestrator.step retry (attempt %d/%d): %w", step.Attempt, maxAttempts, cause) } // markDead 把 step 标 dead、run 标 failed,返回 jobs.Permanent(dead-letter)。 func (h *StepHandler) markDead(ctx context.Context, run *Run, step *Step, cause error) error { msg := cause.Error() step.Status = StepDead step.LastError = &msg _, _ = h.repo.UpdateStepStatus(ctx, step) if _, uerr := h.repo.UpdateRunStatus(ctx, run.ID, RunFailed, &msg); uerr != nil { h.log.Warn("orchestrator.run.mark_failed", "run_id", run.ID, "err", uerr.Error()) } h.recordAudit(ctx, run, "orchestrator.step.dead_letter", step.ID.String(), map[string]any{ "phase": string(step.Phase), "err": msg, }) return jobs.Permanent(cause) } func (h *StepHandler) recordAudit(ctx context.Context, run *Run, action, targetID string, meta map[string]any) { if h.audit == nil { return } uid := run.OwnerID h.audit.Record(ctx, uid, action, targetID, meta) }