package app import ( "context" "strconv" "github.com/google/uuid" "github.com/yan1h/agent-coding-workflow/internal/acp" "github.com/yan1h/agent-coding-workflow/internal/audit" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" "github.com/yan1h/agent-coding-workflow/internal/infra/notify" "github.com/yan1h/agent-coding-workflow/internal/orchestrator" "github.com/yan1h/agent-coding-workflow/internal/project" "github.com/yan1h/agent-coding-workflow/internal/workspace" ) // orchPMAdapter 把 project 的 Repository + RequirementService 适配为 orchestrator.PMAccess。 // 读走 Repository(无需 Caller 鉴权——编排器内部以 run.owner 为权威身份);ChangePhase // 走 RequirementService 以复用阶段网关/审计。 type orchPMAdapter struct { repo project.Repository reqs project.RequirementService adminLookup userAdminAdapter } func (a orchPMAdapter) GetProjectByID(ctx context.Context, id uuid.UUID) (*project.Project, error) { return a.repo.GetProjectByID(ctx, id) } func (a orchPMAdapter) GetProjectBySlug(ctx context.Context, slug string) (*project.Project, error) { return a.repo.GetProjectBySlug(ctx, slug) } func (a orchPMAdapter) GetRequirementByID(ctx context.Context, id uuid.UUID) (*project.Requirement, error) { return a.repo.GetRequirementByID(ctx, id) } func (a orchPMAdapter) GetRequirementByNumber(ctx context.Context, projectID uuid.UUID, number int) (*project.Requirement, error) { return a.repo.GetRequirementByNumber(ctx, projectID, number) } // ListLatestArtifacts 取某需求各阶段最新产物(每阶段最大版本),供 BuildPrompt 注入。 func (a orchPMAdapter) ListLatestArtifacts(ctx context.Context, requirementID uuid.UUID) ([]*project.Artifact, error) { all, err := a.repo.ListArtifactsByRequirement(ctx, requirementID, "") if err != nil { return nil, err } latest := map[project.Phase]*project.Artifact{} for _, art := range all { if art == nil { continue } if cur, ok := latest[art.Phase]; !ok || art.Version > cur.Version { latest[art.Phase] = art } } out := make([]*project.Artifact, 0, len(latest)) for _, ph := range project.AllPhases { if art, ok := latest[ph]; ok { out = append(out, art) } } return out, nil } // ChangeRequirementPhase 以 run.owner 身份推进需求阶段(经 RequirementService 复用网关/审计)。 func (a orchPMAdapter) ChangeRequirementPhase(ctx context.Context, ownerID uuid.UUID, isAdmin bool, projectSlug string, number int, to project.Phase) error { _, err := a.reqs.ChangePhase(ctx, project.Caller{UserID: ownerID, IsAdmin: isAdmin}, projectSlug, number, to) return err } // orchACLAdapter 把 project ACL 适配为 orchestrator.ProjectACL(owner+admin 可写)。 type orchACLAdapter struct { pa projectAccessAdapter } func (a orchACLAdapter) CanRead(ctx context.Context, userID uuid.UUID, isAdmin bool, projectID uuid.UUID) (bool, error) { canRead, _, err := a.pa.ResolveByID(ctx, userID, isAdmin, projectID) if err != nil { return false, err } return canRead, nil } func (a orchACLAdapter) CanWrite(ctx context.Context, userID uuid.UUID, isAdmin bool, projectID uuid.UUID) (bool, error) { _, canWrite, err := a.pa.ResolveByID(ctx, userID, isAdmin, projectID) if err != nil { return false, err } return canWrite, nil } // orchAuditAdapter 把 audit.Recorder 适配为 orchestrator.auditRecorder(窄 record 方法)。 // 注:orchestrator.auditRecorder 是非导出接口,但 app 在同模块外通过结构方法匹配满足。 type orchAuditAdapter struct { rec audit.Recorder } func (a orchAuditAdapter) Record(ctx context.Context, userID uuid.UUID, action, targetID string, meta map[string]any) { if a.rec == nil { return } uid := userID _ = a.rec.Record(ctx, audit.Entry{ UserID: &uid, Action: action, TargetType: "orchestrator_run", TargetID: targetID, Metadata: meta, }) } // orchSessionTermAdapter 把 acp.SessionService 适配为 orchestrator.sessionTerminator。 type orchSessionTermAdapter struct { sess acp.SessionService } func (a orchSessionTermAdapter) TerminateSession(ctx context.Context, ownerID uuid.UUID, isAdmin bool, sessionID uuid.UUID) error { return a.sess.Terminate(ctx, acp.Caller{UserID: ownerID, IsAdmin: isAdmin}, sessionID) } // orchGateArtifactAdapter 把 project.Repository 适配为 orchestrator.GateArtifactAccess。 type orchGateArtifactAdapter struct { repo project.Repository } func (a orchGateArtifactAdapter) ListArtifacts(ctx context.Context, projectID, requirementID uuid.UUID, phase project.Phase) ([]*project.Artifact, error) { return a.repo.ListArtifactsByRequirement(ctx, requirementID, phase) } // orchGateIssueAdapter 把 project.Repository 适配为 orchestrator.GateIssueAccess。 type orchGateIssueAdapter struct { repo project.Repository } func (a orchGateIssueAdapter) CountOpenIssues(ctx context.Context, requirementID uuid.UUID) (int, error) { issues, err := a.repo.ListIssuesByRequirement(ctx, requirementID) if err != nil { return 0, err } n := 0 for _, iss := range issues { if iss != nil && iss.Status == project.StatusOpen { n++ } } return n, nil } // orchACPLookupAdapter 把 acp.Repository 适配为 orchestrator.ACPSessionLookup。 type orchACPLookupAdapter struct { repo acp.Repository } func (a orchACPLookupAdapter) GetSessionByStepID(ctx context.Context, stepID uuid.UUID) (*acp.Session, error) { return a.repo.GetSessionByStepID(ctx, stepID) } func (a orchACPLookupAdapter) GetCrashedSessionForResume(ctx context.Context, stepID uuid.UUID) (*acp.Session, error) { return a.repo.GetCrashedSessionForResume(ctx, stepID) } // 确保 acp.SessionService 同时满足 orchestrator.ACPSessions(Create)。 var _ orchestrator.ACPSessions = (acp.SessionService)(nil) // ===== 任务分解并行调度器适配器(autonomy roadmap §10)===== // schedulerReadySourceAdapter 把 project.Repository 适配为 orchestrator.ReadyTaskSource。 type schedulerReadySourceAdapter struct { repo project.Repository } func (a schedulerReadySourceAdapter) ListSchedulableProjects(ctx context.Context) ([]uuid.UUID, error) { return a.repo.ListSchedulableProjects(ctx) } func (a schedulerReadySourceAdapter) ListReadyLeafSubtasks(ctx context.Context, projectID uuid.UUID, limit int) ([]*project.Issue, error) { return a.repo.ListReadyLeafSubtasks(ctx, projectID, limit) } func (a schedulerReadySourceAdapter) CountActiveSessionsForProject(ctx context.Context, projectID uuid.UUID) (int, error) { return a.repo.CountActiveSessionsForProject(ctx, projectID) } var _ orchestrator.ReadyTaskSource = schedulerReadySourceAdapter{} // schedulerSessionStarterAdapter 为就绪子任务以特权身份(项目 owner)创建 ACP session。 // 解析 workspace(issue.WorkspaceID 优先,否则取项目首个 workspace)与 agent-kind // (implementing 阶段默认),把 IssueNumber 注入使 SessionService 绑定 issue/-N worktree。 type schedulerSessionStarterAdapter struct { sess acp.SessionService projectRepo project.Repository wsRepo workspace.Repository defaultAgentKind func() (uuid.UUID, bool) // 解析 implementing 阶段默认 agent-kind } func (a schedulerSessionStarterAdapter) StartForIssue(ctx context.Context, iss *project.Issue) (uuid.UUID, error) { proj, err := a.projectRepo.GetProjectByID(ctx, iss.ProjectID) if err != nil { return uuid.Nil, err } // 解析 workspace:issue 显式关联优先,否则取项目首个 workspace。 var wsID uuid.UUID if iss.WorkspaceID != nil { wsID = *iss.WorkspaceID } else { list, lerr := a.wsRepo.ListWorkspacesByProject(ctx, iss.ProjectID) if lerr != nil { return uuid.Nil, lerr } if len(list) == 0 { return uuid.Nil, errs.New(errs.CodeInvalidInput, "project 无可用 workspace,无法为子任务派发 session") } wsID = list[0].ID } akID, ok := a.defaultAgentKind() if !ok { return uuid.Nil, errs.New(errs.CodeInvalidInput, "未配置 implementing 阶段默认 agent-kind,无法派发子任务 session") } num := iss.Number owner := acp.Caller{UserID: proj.OwnerID, IsAdmin: false} created, err := a.sess.Create(ctx, owner, acp.CreateSessionInput{ WorkspaceID: wsID, AgentKindID: akID, IssueNumber: &num, }) if err != nil { return uuid.Nil, err } return created.ID, nil } var _ orchestrator.SessionStarter = schedulerSessionStarterAdapter{} // schedulerNotifyAdapter 把 notify.Dispatcher 适配为 orchestrator.SchedulerNotifier。 // 通知发给项目 owner(best-effort,失败不阻塞调度)。 type schedulerNotifyAdapter struct { dispatcher *notify.Dispatcher projectRepo project.Repository } func (a schedulerNotifyAdapter) ownerOf(ctx context.Context, projectID uuid.UUID) (uuid.UUID, bool) { proj, err := a.projectRepo.GetProjectByID(ctx, projectID) if err != nil { return uuid.Nil, false } return proj.OwnerID, true } func (a schedulerNotifyAdapter) NotifyDispatched(ctx context.Context, iss *project.Issue, sessionID uuid.UUID) { if a.dispatcher == nil { return } owner, ok := a.ownerOf(ctx, iss.ProjectID) if !ok { return } _ = a.dispatcher.Dispatch(ctx, notify.Message{ UserID: owner, Topic: "orchestrator.subtask_dispatched", Severity: notify.SeverityInfo, Title: "子任务已派发", Body: "子任务 #" + itoa(iss.Number) + " 已派发到并行 ACP session", Metadata: map[string]any{"issue_number": iss.Number, "session_id": sessionID.String()}, }) } func (a schedulerNotifyAdapter) NotifyQuotaBlocked(ctx context.Context, iss *project.Issue) { if a.dispatcher == nil { return } owner, ok := a.ownerOf(ctx, iss.ProjectID) if !ok { return } _ = a.dispatcher.Dispatch(ctx, notify.Message{ UserID: owner, Topic: "orchestrator.subtask_quota_blocked", Severity: notify.SeverityWarning, Title: "子任务派发受配额限制", Body: "子任务 #" + itoa(iss.Number) + " 因会话配额暂缓派发,下一轮重试", Metadata: map[string]any{"issue_number": iss.Number}, }) } var _ orchestrator.SchedulerNotifier = schedulerNotifyAdapter{} func itoa(n int) string { return strconv.Itoa(n) }