// runner.go 是回合驱动器:给定一个 Step,要么经 acp.SessionService.Create 以特权 // owner 身份创建新 session,要么在崩溃 session 的同一 worktree 上 ResumeSession,然后 // 经 acp.TurnRunner.SendPromptAndWait 阻塞跑完一个回合。封装重试/恢复策略。 package orchestrator import ( "context" "time" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/acp" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" ) // ACPSessions 是 runner 创建 session 所需的窄接口(acp.SessionService 满足)。 type ACPSessions interface { Create(ctx context.Context, c acp.Caller, in acp.CreateSessionInput) (*acp.Session, error) } // ACPSessionLookup 是 runner 查找 step 已有 session 所需的窄接口(acp.Repository 满足)。 type ACPSessionLookup interface { GetSessionByStepID(ctx context.Context, stepID uuid.UUID) (*acp.Session, error) GetCrashedSessionForResume(ctx context.Context, stepID uuid.UUID) (*acp.Session, error) } // turnResult 是一次回合驱动的结果。 type turnResult struct { SessionID uuid.UUID StopReason string } // runner 封装 spawn-or-resume + SendPromptAndWait。 type runner struct { sessions ACPSessions turns acp.TurnRunner lookup ACPSessionLookup turnTimeout time.Duration } // NewRunner 构造回合驱动器(由 app.go 装配 StepHandler 时调用)。turnTimeout<=0 时 // 退回 10 分钟默认值。 func NewRunner(sessions ACPSessions, turns acp.TurnRunner, lookup ACPSessionLookup, turnTimeout time.Duration) *runner { if turnTimeout <= 0 { turnTimeout = 10 * time.Minute } return &runner{sessions: sessions, turns: turns, lookup: lookup, turnTimeout: turnTimeout} } // driveTurn 为 step 驱动一个回合: // // 1. 若该 step 已有崩溃/退出 session → ResumeSession(同 worktree)。 // 2. 否则若该 step 已有活跃 session → 复用(幂等:避免重试重复 spawn)。 // 3. 否则 → Create 一个新 session(特权 owner 身份;OrchestratorStepID 反向关联)。 // // requirementNumber 用于把新 session 绑定到 req/-N worktree,使同一 run 的各 // 阶段 step 共享同一 worktree(resume 也据此回到原 worktree)。 // // 然后 SendPromptAndWait(受 turnTimeout 约束)。返回 sessionID + 原始 stopReason。 func (r *runner) driveTurn(ctx context.Context, run *Run, step *Step, requirementNumber int) (turnResult, error) { owner := acp.Caller{UserID: run.OwnerID, IsAdmin: false} var sessionID uuid.UUID // 1. 崩溃 session → resume(同一 worktree)。 if crashed, err := r.lookup.GetCrashedSessionForResume(ctx, step.ID); err == nil && crashed != nil { resumed, rerr := r.turns.ResumeSession(ctx, owner, crashed.ID) if rerr != nil { return turnResult{}, rerr } sessionID = resumed.ID } else if live, lerr := r.lookup.GetSessionByStepID(ctx, step.ID); lerr == nil && live != nil && live.Status.IsActive() { // 2. 已有活跃 session → 复用。 sessionID = live.ID } else { // 3. 创建新 session(特权 owner;反向关联到 step)。 stepID := step.ID in := acp.CreateSessionInput{ WorkspaceID: run.WorkspaceID, AgentKindID: step.AgentKindID, InitialPrompt: nil, // 由 SendPromptAndWait 单独发送,确保阻塞等待回合 OrchestratorStepID: &stepID, } if requirementNumber > 0 { // 绑定到 req 分支 worktree,使同一 run 的各阶段共享同一 worktree。 rn := requirementNumber in.RequirementNumber = &rn } created, cerr := r.sessions.Create(ctx, owner, in) if cerr != nil { return turnResult{}, cerr } sessionID = created.ID } if sessionID == uuid.Nil { return turnResult{}, errs.New(errs.CodeInternal, "orchestrator: no session id after spawn/resume") } turnCtx, cancel := context.WithTimeout(ctx, r.turnTimeout) defer cancel() stop, err := r.turns.SendPromptAndWait(turnCtx, sessionID, step.Prompt) if err != nil { return turnResult{SessionID: sessionID}, err } return turnResult{SessionID: sessionID, StopReason: stop}, nil }