Files
2026-06-22 08:55:57 +08:00

108 lines
4.0 KiB
Go

// 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/<slug>-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
}