package squad import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path/filepath" "sort" "strings" "sync" "time" "error-code-system/errcode" ) // Options 引擎组装选项(组合根装配;库内零产品依赖)。 type Options struct { DataDir string // 数据根:runs/、roles.d/、blueprints.d/ 所在目录(必填) Registry *RoleRegistry Blueprints *BlueprintRegistry Planner Planner Backends *BackendResolver Evaluator StepEvaluator Checker Checker Sink ProgressSink // DefaultBackend 角色未声明 Backend 时的默认智能体名(默认 stub)。 DefaultBackend string // StepTimeout 单步执行超时(默认 10 分钟)。 StepTimeout time.Duration // HitlPolicy 全局 HITL 策略(角色/Goal 可覆盖)。 HitlPolicy HitlPolicy // RepairBudget 质检失败自动修复次数(默认 1)。 RepairBudget int // IntentGate 可选意图闸门:Run 入口一次确定性裁决(nil = 不启用, // 行为与原实现一致)。见 intent.go;实现方为 squadintent 适配器。 IntentGate IntentGate // IntentCatalog 可选能力目录(squadintent 渲染):注入 PlanInput 与 leader 提示词, // 并用于 intent 步骤的确定性校验。nil = 未启用目录驱动编排。 IntentCatalog *IntentCatalog // IntentRunner 系统功能步执行器(squadintent 提供)。目录启用而 Runner 缺席时, // intent 步骤 fail-closed。 IntentRunner IntentRunner // Router 可选分诊。direct = 单步 assistant,不进多角色;plan = leader 委派。 // nil = 保持调用方传入的 RunOptions.Mode。 Router Router } // engineDeps 驱动器依赖(由引擎从 Options 派生)。 type engineDeps struct { Registry *RoleRegistry Backends *BackendResolver Evaluator StepEvaluator Checker Checker Sink ProgressSink Workspaces *WorkspaceEngine StepTimeout time.Duration HitlPolicy HitlPolicy IntentRunner IntentRunner } // RunOptions 单次运行的可选项。 type RunOptions struct { // TaskKey 显式指定蓝图模板键;空则按 Goal.Text 匹配或自主规划。 TaskKey string `json:"task_key,omitempty"` // Backend 运行级默认后端(覆盖引擎 DefaultBackend)。 Backend string `json:"backend,omitempty"` // Mode 编排模式:standard(默认,一次性计划顺序执行)| leader // (leader-delegation:leader 角色逐步委派成员,成员产出后唤醒 leader 再评估)。 Mode string `json:"mode,omitempty"` // LeaderRole leader 模式下的 leader 角色 key(必须已注册或在计划编制内)。 LeaderRole string `json:"leader_role,omitempty"` // MaxLeaderTurns leader 模式每运行的 leader 轮次上限(默认 8,防失控)。 MaxLeaderTurns int `json:"max_leader_turns,omitempty"` } // SquadEngine 小队引擎门面:Run / Respond / Resume / Inspect / ListRuns。 // 每个运行同一时刻只允许一个控制操作(Run/Respond 互斥)。 type SquadEngine struct { opts Options deps engineDeps ws *WorkspaceEngine mu sync.Mutex runLocks map[string]*sync.Mutex } // NewEngine 组装引擎并应用默认值。 func NewEngine(opts Options) (*SquadEngine, error) { if opts.DataDir == "" { return nil, fmt.Errorf("DataDir is required") } if opts.Registry == nil { opts.Registry = NewRoleRegistry() } if opts.Blueprints == nil { opts.Blueprints = NewBlueprintRegistry() } if opts.Planner == nil { opts.Planner = NewRulePlanner() } if opts.Backends == nil { opts.Backends = NewBackendResolver() } if opts.Evaluator == nil { opts.Evaluator = NoOpEvaluator{} } if opts.Checker == nil { opts.Checker = MinimalChecker{} } if opts.Sink == nil { opts.Sink = NopSink{} } if opts.DefaultBackend == "" { opts.DefaultBackend = "stub" } if opts.StepTimeout == 0 { opts.StepTimeout = 10 * time.Minute } if opts.RepairBudget == 0 { opts.RepairBudget = 1 } if opts.HitlPolicy.Mode == "" { opts.HitlPolicy.Mode = HitlInteractive } ws := &WorkspaceEngine{DataDir: opts.DataDir} deps := engineDeps{ Registry: opts.Registry, Backends: opts.Backends, Evaluator: opts.Evaluator, Checker: opts.Checker, Sink: opts.Sink, Workspaces: ws, StepTimeout: opts.StepTimeout, HitlPolicy: opts.HitlPolicy, IntentRunner: opts.IntentRunner, } return &SquadEngine{opts: opts, deps: deps, ws: ws, runLocks: map[string]*sync.Mutex{}}, nil } // Registry 暴露角色注册表(只读使用)。 func (e *SquadEngine) Registry() *RoleRegistry { return e.opts.Registry } // Blueprints 暴露蓝图注册表。 func (e *SquadEngine) Blueprints() *BlueprintRegistry { return e.opts.Blueprints } func (e *SquadEngine) runLock(runID string) *sync.Mutex { e.mu.Lock() defer e.mu.Unlock() l, ok := e.runLocks[runID] if !ok { l = &sync.Mutex{} e.runLocks[runID] = l } return l } // ErrRunBusy 表示该 run 的文件锁正被占用,且在 ctx 截止前没有拿到。 // 非 unix 平台没有 flock,只保留进程内互斥;跨进程互斥需要 unix。 var ErrRunBusy = errors.New("run is busy") // acquire 进入 run 临界区:先校验 id,再拿进程内锁,然后按 ctx 轮询文件锁。 func (e *SquadEngine) acquire(ctx context.Context, runID string) (func(), error) { if !ValidID(runID) { return nil, invalidID(runID) } if ctx == nil { ctx = context.Background() } mu := e.runLock(runID) mu.Lock() release, err := lockRunFile(ctx, filepath.Join(e.opts.DataDir, "runs", runID)) if err != nil { mu.Unlock() return nil, err } return func() { release() mu.Unlock() }, nil } // Run 发起一次运行:规划(含澄清/确认挂起)→ 执行 → 收口。 func (e *SquadEngine) Run(ctx context.Context, goal Goal, runOpts RunOptions) (*RunResult, error) { if strings.TrimSpace(goal.Text) == "" { return nil, fmt.Errorf("goal text is required") } runID := fmt.Sprintf("run_%s", time.Now().UTC().Format("20060102T150405.000000000")) release, err := e.acquire(ctx, runID) if err != nil { return nil, err } defer release() meta := RunMetadata{ ID: runID, Goal: goal, Status: string(RunPlanning), Backend: firstNonEmpty(runOpts.Backend, e.opts.DefaultBackend), Planner: e.opts.Planner.Name(), CreatedAt: time.Now(), UpdatedAt: time.Now(), Metadata: map[string]string{ "task_key": runOpts.TaskKey, "mode": runOpts.Mode, }, } if runOpts.Mode == "leader" { meta.Metadata["leader_role"] = runOpts.LeaderRole if runOpts.MaxLeaderTurns > 0 { meta.Metadata["max_leader_turns"] = fmt.Sprintf("%d", runOpts.MaxLeaderTurns) } } e.deps.Sink.Emit(Event{Type: EvRunStarted, RunID: runID, Text: goal.Text, At: time.Now()}) // 意图闸门(可选,入口一次裁决;显式 TaskKey 优先,不重复过闸)。 // nil 决策 = 不干预走原路径;闸门系统错误与守卫拒绝同样 fail-closed 成失败运行。 if e.opts.IntentGate != nil && runOpts.TaskKey == "" { dec, derr := e.opts.IntentGate.Decide(ctx, goal, runOpts) if derr != nil { e.deps.Sink.Emit(Event{Type: EvRunFailed, RunID: runID, Text: derr.Error(), At: time.Now()}) return e.failNew(&meta, derr) } if dec != nil { if rej := dec.Reject; rej != nil { if dec.IntentID != "" { meta.Metadata["intent_id"] = dec.IntentID } err := errcode.New(errcode.SquadIntentRefused, rej.Reason, "guidance", rej.Guidance) e.deps.Sink.Emit(Event{Type: EvRunFailed, RunID: runID, Text: err.Error(), At: time.Now()}) return e.failNew(&meta, err) } if dec.BlueprintKey != "" { if _, ok := e.opts.Blueprints.Get(dec.BlueprintKey); !ok { err := errcode.New(errcode.SquadBlueprintNotFound, fmt.Sprintf("意图 %s 定向的蓝图 %s 不存在", dec.IntentID, dec.BlueprintKey)) e.deps.Sink.Emit(Event{Type: EvRunFailed, RunID: runID, Text: err.Error(), At: time.Now()}) return e.failNew(&meta, err) } runOpts.TaskKey = dec.BlueprintKey meta.Metadata["intent_id"] = dec.IntentID meta.Metadata["intent_blueprint"] = dec.BlueprintKey } } } st := &runState{Meta: meta, Approved: map[string]bool{}, RepairBudgetRemaining: e.opts.RepairBudget} return e.planAndExecute(ctx, st, runOpts) } // planAndExecute 规划 → 挂起或执行;st 可为已有运行状态(澄清续跑复用)。 func (e *SquadEngine) planAndExecute(ctx context.Context, st *runState, runOpts RunOptions) (*RunResult, error) { drv := &flowDriver{opts: e.deps} meta := &st.Meta if e.opts.Router != nil && runOpts.Mode == "" && meta.Metadata["route"] == "" { dec, rerr := e.opts.Router.Route(ctx, meta.Goal) if rerr != nil { return e.failNew(meta, rerr) } applyRoute(meta, dec, runOpts.Mode) } var planOut *PlanOutput var err error if meta.Metadata["route_direct"] == "1" { plan, perr := directAssistantPlan(e.opts.Registry, meta.Goal) if perr != nil { return e.failNew(meta, perr) } planOut = &PlanOutput{Plan: plan} } else { bpMatch, _ := e.opts.Blueprints.Match(meta.Goal.Text, runOpts.TaskKey) planOut, err = e.opts.Planner.Plan(ctx, PlanInput{ Goal: meta.Goal, Blueprint: bpPtr(bpMatch), Registry: e.opts.Registry, RunID: meta.ID, IntentCatalog: e.opts.IntentCatalog, }) if err != nil { return e.failNew(meta, err) } } // 目录驱动编排的确定性校验:intent 步骤命中目录、必填槽位齐、枚举合法(fail-closed)。 if err := validateIntentSteps(planOut.Plan, e.opts.IntentCatalog); err != nil { e.deps.Sink.Emit(Event{Type: EvRunFailed, RunID: meta.ID, Text: err.Error(), At: time.Now()}) return e.failNew(meta, err) } // 规划歧义:挂起澄清(auto/disabled 模式直接失败,不猜计划)。 if planOut.Clarification != nil { if drv.policy(st).mode() != HitlInteractive { return e.failNew(meta, fmt.Errorf("planner requested clarification but hitl mode is %s", st.policy().mode())) } var qs []string for _, q := range planOut.Clarification.Questions { qs = append(qs, "- "+q.Text) } opts := []HumanOption{} if len(planOut.Clarification.Questions) == 1 && len(planOut.Clarification.Questions[0].Options) > 0 { opts = planOut.Clarification.Questions[0].Options } req := &HumanRequest{ ID: newRequestID(), RunID: meta.ID, Kind: HitlClarification, Severity: SeverityActionRequired, Question: "规划器无法确定如何执行该目标,请补充信息:", Options: opts, Context: fmt.Sprintf("原因:%s\n问题:\n%s", planOut.Clarification.Reason, strings.Join(qs, "\n")), CreatedAt: time.Now(), Status: "pending", } gate := &hitlGate{Request: *req, PrevStatus: RunPlanning} // 澄清挂起也需要工作区承载 pending.json。 if err := e.materializeWorkspace(meta, st, &SquadPlan{Workflow: WorkflowPlan{WorkflowID: NewPlanID(), Status: "pending", Steps: nil}}); err != nil { return nil, err } if err := st.saveGate(gate); err != nil { return nil, err } st.Meta.Status = string(RunAwaitingClarification) _ = st.save() e.deps.Sink.Emit(Event{Type: EvHumanInputRequired, RunID: meta.ID, Text: req.Question, Data: marshalJSON(req), At: time.Now()}) return &RunResult{RunID: meta.ID, Status: RunAwaitingClarification, Pending: req, WorkDir: st.Meta.WorkDir}, nil } // 计划确认:interactive 模式先挂起。leader 模式例外——规划步骤会被 // leader 委派循环取代,派生编制本身不构成执行承诺,无需确认。 if planOut.NeedsConfirmation && drv.policy(st).mode() == HitlInteractive && meta.Metadata["mode"] != "leader" { if err := e.materializeWorkspace(meta, st, planOut.Plan); err != nil { return nil, err } var lines []string for _, s := range planOut.Plan.Workflow.Steps { lines = append(lines, fmt.Sprintf("- %s [%s] %s(产出 %v)", s.ID, s.Role, s.Title, s.Targets)) } var roles []string for _, a := range planOut.Plan.Squad.Agents { roles = append(roles, fmt.Sprintf("- %s(%s,backend=%s model=%s)", a.Role, a.Name, a.Backend, a.Model)) } req := &HumanRequest{ ID: newRequestID(), RunID: meta.ID, Kind: HitlConfirmation, Severity: SeverityActionRequired, Question: "计划已生成,需要人工确认后执行:", Options: []HumanOption{ {Value: "approve", Label: "批准并执行"}, {Value: "abort", Label: "放弃执行"}, }, Context: fmt.Sprintf("确认原因:%s\n编制:\n%s\n步骤:\n%s", planOut.ConfirmReason, strings.Join(roles, "\n"), strings.Join(lines, "\n")), CreatedAt: time.Now(), Status: "pending", Reason: "plan", } gate := &hitlGate{Request: *req, Plan: planOut.Plan, PrevStatus: RunPlanning} if err := st.saveGate(gate); err != nil { return nil, err } st.Meta.Status = string(RunAwaitingConfirmation) _ = st.save() e.deps.Sink.Emit(Event{Type: EvHumanInputRequired, RunID: meta.ID, Text: req.Question, Data: marshalJSON(req), At: time.Now()}) return &RunResult{RunID: meta.ID, Status: RunAwaitingConfirmation, Pending: req, Plan: &planOut.Plan.Workflow, WorkDir: st.Meta.WorkDir}, nil } if err := e.materializeWorkspace(meta, st, planOut.Plan); err != nil { return nil, err } // leader-delegation 模式:忽略规划步骤,进 leader 委派循环。 if meta.Metadata["mode"] == "leader" { e.initLeaderState(st) out, gate, err := e.runLeaderLoop(ctx, st) if err != nil { return nil, err } if gate == nil { e.finalizeEvent(out) } return out, nil } out, _, err := drv.advance(ctx, st) if err != nil { return nil, err } e.finalizeEvent(out) return out, nil } // initLeaderState 按运行选项初始化 leader 循环状态。 func (e *SquadEngine) initLeaderState(st *runState) { leaderRole := st.Meta.Metadata["leader_role"] if leaderRole == "" { if _, ok := findAgent(st.Plan.Squad.Agents, "leader"); ok { leaderRole = "leader" } } maxTurns := DefaultMaxLeaderTurns if v := st.Meta.Metadata["max_leader_turns"]; v != "" { if n := parseIntDefault(v, DefaultMaxLeaderTurns); n > 0 { maxTurns = n } } st.Leader = &LeaderState{LeaderRole: leaderRole, MaxTurns: maxTurns} st.saveLeader() } func parseIntDefault(s string, def int) int { n := 0 for _, c := range s { if c < '0' || c > '9' { return def } n = n*10 + int(c-'0') } return n } // isLeaderRun 该运行是否处于 leader-delegation 模式。 func (e *SquadEngine) isLeaderRun(st *runState) bool { return st.Meta.Metadata["mode"] == "leader" } // resumeAfterHold 挂起解除后的统一续跑入口(按模式分发)。 func (e *SquadEngine) resumeAfterHold(ctx context.Context, st *runState) (*RunResult, error) { if e.isLeaderRun(st) { out, gate, err := e.runLeaderLoop(ctx, st) if err != nil { return nil, err } if gate == nil { e.finalizeEvent(out) } return out, nil } drv := &flowDriver{opts: e.deps} out, _, err := drv.advance(ctx, st) if err != nil { return nil, err } e.finalizeEvent(out) return out, nil } // materializeWorkspace 落盘工作区与子工作区(已存在则只更新计划并重建待执行子目录)。 func (e *SquadEngine) materializeWorkspace(meta *RunMetadata, st *runState, plan *SquadPlan) error { if st.Meta.WorkDir == "" { st.Meta = *meta wsDir, err := e.ws.PrepareWorkspace(&st.Meta, plan) if err != nil { return err } st.Template = plan.Template bb, _ := loadBlackboard(wsDir) st.BB = *bb } else { // 复用既有工作区:磁盘黑板为权威(澄清续跑/计划更新场景)。 if bb, err := loadBlackboard(st.Meta.WorkDir); err == nil { st.BB = *bb } if st.Template == "" { st.Template = plan.Template } } st.Plan = *plan // 编制补全:步骤引用但编制缺失的角色从注册表解析(蓝图/派生计划常见)。 if err := ensureSquadCoversSteps(&st.Plan, e.opts.Registry); err != nil { return err } if err := e.ws.BuildSubWorkspaces(st.Meta.WorkDir, &st.Plan, &st.BB); err != nil { return err } return st.save() } // ensureSquadCoversSteps 确保编制覆盖全部步骤角色。 func ensureSquadCoversSteps(plan *SquadPlan, reg *RoleRegistry) error { for _, step := range plan.Workflow.Steps { if _, ok := findAgent(plan.Squad.Agents, step.Role); ok { continue } spec, ok := reg.Get(step.Role) if !ok { return fmt.Errorf("step %s references role %s not registered", step.ID, step.Role) } plan.Squad.Agents = append(plan.Squad.Agents, spec) } return nil } func (e *SquadEngine) failNew(meta *RunMetadata, err error) (*RunResult, error) { return &RunResult{RunID: meta.ID, Status: RunFailed, Error: err.Error()}, nil } // Respond 应用人工决策并续跑。按请求 ID 幂等:重复回应同一请求返回当前状态。 func (e *SquadEngine) Respond(ctx context.Context, runID string, decision RespondDecision) (*RunResult, error) { release, err := e.acquire(ctx, runID) if err != nil { return nil, err } defer release() return e.respondLocked(ctx, runID, decision) } func (e *SquadEngine) respondLocked(ctx context.Context, runID string, decision RespondDecision) (*RunResult, error) { wsDir := filepath.Join(e.opts.DataDir, "runs", runID) st, err := loadRunState(wsDir) if err != nil { return nil, fmt.Errorf("load run %s: %w", runID, err) } gate, err := loadGate(wsDir) if err != nil { return nil, err } if gate == nil { return nil, fmt.Errorf("run %s has no pending human request", runID) } if decision.RequestID != "" && decision.RequestID != gate.Request.ID { // 幂等:回应已处理过的请求 → 返回当前状态。 if gate.Request.Status != "pending" { return e.Inspect(runID) } return nil, fmt.Errorf("request id mismatch: expected %s", gate.Request.ID) } req := &gate.Request if strings.TrimSpace(decision.Choice) == "" && strings.TrimSpace(decision.Answer) != "" && choiceHoldKind(req.Kind) { return e.noteAndKeepHold(st, gate, decision) } req.Status = "answered" now := time.Now() req.AnsweredAt = &now if err := st.saveGate(gate); err != nil { return nil, err } e.deps.Sink.Emit(Event{Type: EvHumanResponded, RunID: runID, Text: firstNonEmpty(decision.Choice, decision.Answer), Data: marshalJSON(decision), At: time.Now()}) res, err := e.applyDecision(ctx, st, gate, decision) if err != nil { req.Status = "pending" req.AnsweredAt = nil _ = st.saveGate(gate) return nil, err } if err := clearSameGate(st, req.ID); err != nil { return nil, err } return res, nil } func choiceHoldKind(kind HumanRequestKind) bool { switch kind { case HitlConfirmation, HitlPermission, HitlQualityReview: return true default: return false } } func (e *SquadEngine) noteAndKeepHold(st *runState, gate *hitlGate, decision RespondDecision) (*RunResult, error) { if err := st.addSteering(decision.Answer); err != nil { return nil, err } gate.Request.Status = "pending" gate.Request.AnsweredAt = nil if err := st.saveGate(gate); err != nil { return nil, err } return heldResult(st, gate), nil } func clearSameGate(st *runState, requestID string) error { current, err := loadGate(st.Meta.WorkDir) if err != nil || current == nil || current.Request.ID != requestID { return err } return st.saveGate(nil) } func (e *SquadEngine) applyDecision(ctx context.Context, st *runState, gate *hitlGate, decision RespondDecision) (*RunResult, error) { req := &gate.Request now := time.Now() drv := &flowDriver{opts: e.deps} switch req.Kind { case HitlClarification: return e.respondClarification(ctx, st, gate, decision) case HitlConfirmation: if req.Reason == "interrupted" { return e.respondInterrupted(ctx, st, req, decision) } if req.StepID != "" && gate.Plan == nil { return e.respondStepApproval(ctx, st, req, decision) } switch strings.ToLower(decision.Choice) { case "approve": if gate.Plan != nil { if err := e.materializeWorkspace(&st.Meta, st, gate.Plan); err != nil { return nil, err } } if e.isLeaderRun(st) { e.initLeaderState(st) } st.Meta.Status = string(RunExecuting) _ = st.save() return e.resumeAfterHold(ctx, st) default: // abort / reject return e.cancelRun(st, "人工拒绝计划"+appendAnswer(decision)), nil } case HitlGap: switch strings.ToLower(decision.Choice) { case "resolved", "": answer := decision.Answer for _, g := range st.BB.UnresolvedBlockingGaps() { st.BB.ResolveGapByID(g.ID, answerFor(g, answer)) } if answer != "" { st.Clarifications = append(st.Clarifications, "缺口补充:"+answer) } case "skip": for i := range st.BB.Gaps { st.BB.Gaps[i].Blocking = false } case "abort": return e.cancelRun(st, "人工中止(阻塞缺口)"), nil default: return nil, fmt.Errorf("unknown choice %q for gap request", decision.Choice) } st.Meta.Status = string(RunExecuting) _ = st.save() return e.resumeAfterHold(ctx, st) case HitlEscalation: switch strings.ToLower(decision.Choice) { case "abort": return e.cancelRun(st, "人工中止(leader 升级问人)"), nil default: // 自由文本回答 = 人的决定 answer := strings.TrimSpace(decision.Answer) if answer == "" { for _, o := range gate.Request.Options { if strings.EqualFold(o.Value, decision.Choice) { answer = o.Label } } } if answer != "" { st.Clarifications = append(st.Clarifications, "升级问答:"+answer) st.BB.Facts = append(st.BB.Facts, FactItem{ ID: fmt.Sprintf("fact_%d", len(st.BB.Facts)+1), Claim: answer, Source: "human", }) if st.Meta.WorkDir != "" { _ = e.ws.UpdateGoalFile(st.Meta.WorkDir, &st.Meta.Goal) } } st.Meta.Status = string(RunExecuting) _ = st.save() return e.resumeAfterHold(ctx, st) } case HitlAdjustment: switch strings.ToLower(decision.Choice) { case "approve": applyAdjustment(st, gate.Steps, gate.SkipIDs) case "reject", "": // 忽略调整 case "abort": return e.cancelRun(st, "人工中止(拒绝动态调整)"), nil default: return nil, fmt.Errorf("unknown choice %q for adjustment request", decision.Choice) } st.Meta.Status = string(RunExecuting) _ = st.save() return e.resumeAfterHold(ctx, st) case HitlPermission: switch strings.ToLower(decision.Choice) { case "approve": // 授权记到角色(gate 上下文带角色名)。 role := gate.Request.StepID for _, s := range st.Plan.Workflow.Steps { if s.ID == gate.Request.StepID { role = s.Role } } st.Approved["role:"+role] = true st.Meta.Status = string(RunExecuting) _ = st.save() return e.resumeAfterHold(ctx, st) case "reject": // 步骤失败(关键性语义由 failStep 处理)。 for i := range st.Plan.Workflow.Steps { if st.Plan.Workflow.Steps[i].ID == gate.Request.StepID { st.Plan.Workflow.Steps[i].Status = "failed" st.Plan.Workflow.Steps[i].Error = "permission denied by human" st.Meta.Status = string(RunExecuting) _ = st.save() out, _, err := drv.failStepResume(ctx, st, i) if err != nil { return nil, err } e.finalizeEvent(out) return out, nil } } return e.cancelRun(st, "权限被拒绝且步骤不可定位"), nil default: return e.cancelRun(st, "人工中止(权限审批)"), nil } case HitlQualityReview: switch strings.ToLower(decision.Choice) { case "approve": return e.manualPass(st, decision) case "retry": // 追加一次修复预算并重跑交付步骤。 st.RepairBudgetRemaining++ st.Meta.Status = string(RunExecuting) _ = st.save() deliverStepID := lastCompletedStepID(st) out, _, err := drv.rerunForRepair(ctx, st, deliverStepID) if err != nil { return nil, err } e.finalizeEvent(out) return out, nil default: st.Meta.Status = string(RunFailed) st.Meta.CompletedAt = &now wf := st.Workflow() wf.Status = "failed" st.setWorkflow(wf) _ = st.save() e.deps.Sink.Emit(Event{Type: EvRunFailed, RunID: st.Meta.ID, Text: "人工裁决质检失败", At: time.Now()}) return &RunResult{RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st), Error: "quality review rejected by human", WorkDir: st.Meta.WorkDir}, nil } default: return nil, fmt.Errorf("unsupported request kind %q", req.Kind) } } // respondClarification 澄清回应:答案并入目标后重新规划。 func (e *SquadEngine) respondClarification(ctx context.Context, st *runState, gate *hitlGate, decision RespondDecision) (*RunResult, error) { choice := strings.ToLower(decision.Choice) if choice == "abort" { return e.cancelRun(st, "人工中止(澄清阶段)"), nil } answer := strings.TrimSpace(decision.Answer) if answer == "" && len(gate.Request.Options) > 0 { // 无自由文本时尝试取选项 Label。 for _, o := range gate.Request.Options { if strings.EqualFold(o.Value, decision.Choice) { answer = o.Label } } } if answer != "" { st.Clarifications = append(st.Clarifications, answer) st.Meta.Goal.Text = st.Meta.Goal.Text + "\n【人工澄清】" + answer if st.Meta.WorkDir != "" { _ = e.ws.UpdateGoalFile(st.Meta.WorkDir, &st.Meta.Goal) } } if choice == "" || choice == "continue" || choice == "minimal" { if choice == "minimal" { // 人工选择最小编制:引导注册 4 个通用角色(backend 用运行默认)。 e.bootstrapMinimalSquad(st) } st.Meta.Status = string(RunPlanning) _ = st.save() runOpts := RunOptions{TaskKey: st.Meta.Metadata["task_key"]} return e.planAndExecute(ctx, st, runOpts) } return e.cancelRun(st, "人工中止(澄清阶段)"), nil } // bootstrapMinimalSquad 注册通用最小编制(organizer/specialist/writer/leader)。 func (e *SquadEngine) bootstrapMinimalSquad(st *runState) { backend := st.Meta.Backend if backend == "" { backend = e.opts.DefaultBackend } for _, spec := range []RoleSpec{ {Role: "organizer", Name: "梳理专员", Backend: backend, Keywords: []string{"整理", "梳理", "事实"}, Responsibilities: []string{"梳理目标背景与输入,回填事实与缺口"}}, {Role: "specialist", Name: "专项专员", Backend: backend, Keywords: []string{"分析", "推演", "方案"}, Responsibilities: []string{"围绕目标开展专业分析与推演"}}, {Role: "writer", Name: "撰写专员", Backend: backend, Keywords: []string{"撰写", "成文", "报告"}, Responsibilities: []string{"汇总产出并撰写交付物"}}, {Role: "leader", Name: "审核专员", Backend: backend, Keywords: []string{"审核", "终审", "把关"}, Responsibilities: []string{"一致性终审与交付把关"}}, } { _ = e.opts.Registry.Register(spec) } } // manualPass 人工放行质检:标记 ManualPass 并收口。 func (e *SquadEngine) manualPass(st *runState, decision RespondDecision) (*RunResult, error) { report := e.deps.Checker.Check(CheckRequest{ RunID: st.Meta.ID, Workspace: st.Meta.WorkDir, Deliverable: "output.md", Plan: planPtr(st), Squad: &st.Plan.Squad, Template: st.Template, }) report.ManualPass = true note := strings.TrimSpace(decision.Answer) if note != "" { report.Violations = append(report.Violations, "人工放行说明:"+note) } status := RunCompleted if countStatus(st, "failed") > 0 || countStatus(st, "skipped") > 0 { status = RunDegraded } now := time.Now() st.Meta.Status = string(status) st.Meta.CompletedAt = &now wf := st.Workflow() wf.Status = string(status) st.setWorkflow(wf) _ = st.save() manifest := buildManifest(st, "output.md", *report) _ = writeJSON(filepath.Join(st.Meta.WorkDir, "manifest.json"), manifest) e.deps.Sink.Emit(Event{Type: EvRunCompleted, RunID: st.Meta.ID, Text: string(status), At: time.Now()}) return &RunResult{RunID: st.Meta.ID, Status: status, Plan: planPtr(st), Deliverable: "output.md", Quality: report, WorkDir: st.Meta.WorkDir, Summary: summarize(st, *report, status) + "(质检人工放行)"}, nil } func (e *SquadEngine) cancelRun(st *runState, reason string) *RunResult { now := time.Now() st.Meta.Status = string(RunCancelled) st.Meta.CompletedAt = &now wf := st.Workflow() wf.Status = "cancelled" st.setWorkflow(wf) _ = st.save() e.deps.Sink.Emit(Event{Type: EvRunCancelled, RunID: st.Meta.ID, Text: reason, At: time.Now()}) return &RunResult{RunID: st.Meta.ID, Status: RunCancelled, Plan: planPtr(st), Summary: reason, WorkDir: st.Meta.WorkDir} } // failStepResume 权限拒绝后的失败步骤处理(关键失败/非关键降级)。 func (d *flowDriver) failStepResume(ctx context.Context, st *runState, idx int) (*RunResult, *hitlGate, error) { return d.failStep(ctx, st, idx) } func (e *SquadEngine) respondStepApproval(ctx context.Context, st *runState, req *HumanRequest, decision RespondDecision) (*RunResult, error) { switch strings.ToLower(decision.Choice) { case "approve": st.Approved["step:"+req.StepID] = true case "skip": if !markStep(st, req.StepID, "skipped", "") { return nil, fmt.Errorf("step %s not found", req.StepID) } default: return e.cancelRun(st, "人工中止(步骤审批)"), nil } st.Meta.Status = string(RunExecuting) if err := st.save(); err != nil { return nil, err } return e.resumeAfterHold(ctx, st) } func (e *SquadEngine) respondInterrupted(ctx context.Context, st *runState, req *HumanRequest, decision RespondDecision) (*RunResult, error) { switch strings.ToLower(decision.Choice) { case "retry": if !markStep(st, req.StepID, "pending", "") { return nil, fmt.Errorf("step %s not found", req.StepID) } case "skip": if !markStep(st, req.StepID, "skipped", "") { return nil, fmt.Errorf("step %s not found", req.StepID) } default: return e.cancelRun(st, "人工中止(中断恢复)"), nil } st.Meta.Status = string(RunExecuting) if err := st.save(); err != nil { return nil, err } return e.resumeAfterHold(ctx, st) } func markStep(st *runState, id, status, errText string) bool { for i := range st.Plan.Workflow.Steps { if st.Plan.Workflow.Steps[i].ID != id { continue } st.Plan.Workflow.Steps[i].Status = status st.Plan.Workflow.Steps[i].Error = errText return true } return false } // recoverInterrupted 处理崩溃时停在 running 的步骤。 // interactive 挂起确认,不自动再执行;auto 允许一次重试;disabled 直接失败。 func (e *SquadEngine) recoverInterrupted(ctx context.Context, st *runState) (*RunResult, error) { for i := range st.Plan.Workflow.Steps { step := st.Plan.Workflow.Steps[i] if step.Status != "running" { continue } switch st.policy().mode() { case HitlDisabled: st.Plan.Workflow.Steps[i].Status = "failed" st.Plan.Workflow.Steps[i].Error = "interrupted" if err := st.save(); err != nil { return nil, err } drv := &flowDriver{opts: e.deps} out, _, err := drv.failStep(ctx, st, i) return out, err case HitlAuto: if step.Attempts >= 2 { st.Plan.Workflow.Steps[i].Status = "failed" st.Plan.Workflow.Steps[i].Error = "interrupted twice" if err := st.save(); err != nil { return nil, err } drv := &flowDriver{opts: e.deps} out, _, err := drv.failStep(ctx, st, i) return out, err } st.Plan.Workflow.Steps[i].Status = "pending" if err := st.save(); err != nil { return nil, err } return nil, nil default: drv := &flowDriver{opts: e.deps} out, _, err := drv.hold(st, HitlConfirmation, step.ID, step.Role, fmt.Sprintf("步骤「%s」在执行中中断。如何继续?", step.Title), []HumanOption{ {Value: "retry", Label: "重试"}, {Value: "skip", Label: "跳过该步骤"}, {Value: "abort", Label: "中止运行"}, }, "interrupted", "interrupted", nil) return out, err } } return nil, nil } // Resume 恢复运行:有挂起则返回挂起状态;未完成则继续推进(重启恢复入口)。 func (e *SquadEngine) Resume(ctx context.Context, runID string) (*RunResult, error) { release, err := e.acquire(ctx, runID) if err != nil { return nil, err } defer release() wsDir := filepath.Join(e.opts.DataDir, "runs", runID) st, err := loadRunState(wsDir) if err != nil { return nil, fmt.Errorf("load run %s: %w", runID, err) } if gate, _ := loadGate(wsDir); gate != nil && gate.Request.Status == "pending" { return &RunResult{RunID: runID, Status: RunStatus(st.Meta.Status), Plan: planPtr(st), Pending: &gate.Request, WorkDir: st.Meta.WorkDir}, nil } if RunStatus(st.Meta.Status).Terminal() { return e.Inspect(runID) } if out, err := e.recoverInterrupted(ctx, st); out != nil || err != nil { return out, err } return e.resumeAfterHold(ctx, st) } // Inspect 查看运行当前状态(含挂起请求与黑板)。 func (e *SquadEngine) Inspect(runID string) (*RunResult, error) { if !ValidID(runID) { return nil, invalidID(runID) } wsDir := filepath.Join(e.opts.DataDir, "runs", runID) st, err := loadRunState(wsDir) if err != nil { return nil, fmt.Errorf("load run %s: %w", runID, err) } out := &RunResult{ RunID: runID, Status: RunStatus(st.Meta.Status), Plan: planPtr(st), Squad: &st.Plan.Squad, WorkDir: st.Meta.WorkDir, Summary: st.Meta.Status, } if gate, _ := loadGate(wsDir); gate != nil { out.Pending = &gate.Request } if data, err := os.ReadFile(filepath.Join(wsDir, "output.md")); err == nil && len(data) > 0 { out.Deliverable = "output.md" } if data, err := os.ReadFile(filepath.Join(wsDir, "manifest.json")); err == nil { var m Manifest if json.Unmarshal(data, &m) == nil { out.Quality = &m.QualityReport } } return out, nil } // Blackboard 查看运行黑板。 func (e *SquadEngine) Blackboard(runID string) (*BlackboardState, error) { if !ValidID(runID) { return nil, invalidID(runID) } wsDir := filepath.Join(e.opts.DataDir, "runs", runID) return loadBlackboard(wsDir) } // ListRuns 列出全部运行(按创建时间倒序)。 func (e *SquadEngine) ListRuns() ([]RunMetadata, error) { runsDir := filepath.Join(e.opts.DataDir, "runs") entries, err := os.ReadDir(runsDir) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var out []RunMetadata for _, ent := range entries { if !ent.IsDir() { continue } st, err := loadRunState(filepath.Join(runsDir, ent.Name())) if err != nil { continue } out = append(out, st.Meta) } sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt.After(out[j].CreatedAt) }) return out, nil } // Pending 返回运行当前挂起请求(无则 nil)。 func (e *SquadEngine) Pending(runID string) (*HumanRequest, error) { if !ValidID(runID) { return nil, invalidID(runID) } gate, err := loadGate(filepath.Join(e.opts.DataDir, "runs", runID)) if err != nil || gate == nil { return nil, err } return &gate.Request, nil } // Steer 向运行中的任务追加人工补充指令:在下一次步骤边界注入当前运行的 // prompt(步骤间注入,不中断正在执行的步骤),落盘 steering.json。 func (e *SquadEngine) Steer(runID, text string) error { if strings.TrimSpace(text) == "" { return fmt.Errorf("steering text is empty") } release, err := e.acquire(context.Background(), runID) if err != nil { return err } defer release() st, err := loadRunState(filepath.Join(e.opts.DataDir, "runs", runID)) if err != nil { return fmt.Errorf("load run %s: %w", runID, err) } if RunStatus(st.Meta.Status).Terminal() { return fmt.Errorf("run %s already terminal (%s)", runID, st.Meta.Status) } st.Steering = append(st.Steering, strings.TrimSpace(text)) _ = writeJSON(filepath.Join(st.Meta.WorkDir, "steering.json"), st.Steering) e.deps.Sink.Emit(Event{Type: "steered", RunID: runID, Text: truncate(text, 80), At: time.Now()}) return nil } // SetBlackboardField 写入黑板字段(外部事实注入;可满足 field_set 等待条件)。 func (e *SquadEngine) SetBlackboardField(runID, key, value string) error { if strings.TrimSpace(key) == "" { return fmt.Errorf("field key is empty") } release, err := e.acquire(context.Background(), runID) if err != nil { return err } defer release() st, err := loadRunState(filepath.Join(e.opts.DataDir, "runs", runID)) if err != nil { return fmt.Errorf("load run %s: %w", runID, err) } st.BB.Fields[key] = value if err := saveBlackboard(st.Meta.WorkDir, &st.BB); err != nil { return err } e.deps.Sink.Emit(Event{Type: "blackboard_field_set", RunID: runID, Text: key + "=" + truncate(value, 60), At: time.Now()}) return nil } // SweepWakeups 扫描所有挂起运行:等待条件(run_done/field_set)满足时 // 合成自动决策续跑。护栏:同一请求自动唤醒次数达 MaxWakeupFires 即自动中止。 // 由 serve 模式周期调用,也可在任何运行终态后手动调用。返回唤醒数。 func (e *SquadEngine) SweepWakeups(ctx context.Context) int { runs, err := e.ListRuns() if err != nil { return 0 } woken := 0 for _, meta := range runs { if !RunStatus(meta.Status).IsHeld() { continue } gate, err := loadGate(filepath.Join(e.opts.DataDir, "runs", meta.ID)) if err != nil || gate == nil || gate.Request.Status != "pending" || gate.Request.WaitCondition == nil { continue } release, err := e.acquire(ctx, meta.ID) if err != nil { continue } if e.fireWakeup(ctx, meta.ID) { woken++ } release() } return woken } // SweepExpired 把超过 HITL 时限的挂起请求合成 fail 或 cancel 决策。 func (e *SquadEngine) SweepExpired(ctx context.Context, now time.Time) int { runs, err := e.ListRuns() if err != nil { return 0 } n := 0 for _, meta := range runs { if !RunStatus(meta.Status).IsHeld() { continue } gate, err := loadGate(filepath.Join(e.opts.DataDir, "runs", meta.ID)) if err != nil || gate == nil || gate.Request.Status != "pending" { continue } decision, due := expiredDecision(gate.Request, e.deps.HitlPolicy, now) if !due { continue } if _, err := e.Respond(ctx, meta.ID, decision); err == nil { n++ } } return n } // fireWakeup 在已持有 run 锁时处理一条挂起。只自动放行 gap 与 escalation。 func (e *SquadEngine) fireWakeup(ctx context.Context, runID string) bool { gate, err := loadGate(filepath.Join(e.opts.DataDir, "runs", runID)) if err != nil || gate == nil || gate.Request.Status != "pending" || gate.Request.WaitCondition == nil { return false } met, fact := e.evaluateWaitCondition(runID, gate.Request.WaitCondition) if !met { return false } gate.Request.WakeupFires++ if err := writeJSON(filepath.Join(e.opts.DataDir, "runs", runID, "pending.json"), gate); err != nil { return false } if gate.Request.WakeupFires >= MaxWakeupFires { _, err := e.respondLocked(ctx, runID, RespondDecision{RequestID: gate.Request.ID, Choice: "abort", Answer: "自动唤醒次数达上限,自动中止"}) return err == nil } switch gate.Request.Kind { case HitlGap, HitlEscalation: decision := RespondDecision{RequestID: gate.Request.ID, Choice: "resolved", Answer: "[自动唤醒] " + fact} if _, err := e.respondLocked(ctx, runID, decision); err != nil { return false } e.deps.Sink.Emit(Event{Type: "wakeup_fired", RunID: runID, Text: fact, At: time.Now()}) return true default: e.deps.Sink.Emit(Event{Type: "wakeup_held", RunID: runID, Text: fact, At: time.Now()}) return false } } // evaluateWaitCondition 评估等待条件;返回是否满足与事实描述。 func (e *SquadEngine) evaluateWaitCondition(runID string, cond *WaitCondition) (bool, string) { switch cond.Type { case "run_done": if cond.RunID == "" || cond.RunID == runID { return false, "" // 自引用不触发 } target, err := e.Inspect(cond.RunID) if err != nil { return false, "" } if target.Status.Terminal() { return true, fmt.Sprintf("等待的运行 %s 已进入终态 %s(唤醒报告事实,非业务完成)", cond.RunID, target.Status) } case "field_set": if cond.Field == "" { return false, "" } bb, err := e.Blackboard(runID) if err != nil { return false, "" } if v, ok := bb.Fields[cond.Field]; ok && strings.TrimSpace(v) != "" { return true, fmt.Sprintf("黑板字段 %s 已设置:%s", cond.Field, truncate(v, 80)) } } return false, "" } func (e *SquadEngine) finalizeEvent(out *RunResult) { if out == nil { return } switch out.Status { case RunCompleted, RunDegraded: e.deps.Sink.Emit(Event{Type: EvRunCompleted, RunID: out.RunID, Text: out.Summary, At: time.Now()}) case RunFailed: e.deps.Sink.Emit(Event{Type: EvRunFailed, RunID: out.RunID, Text: out.Error, At: time.Now()}) } } // ---- 小工具 ---- func sha256File(path string) string { data, err := os.ReadFile(path) if err != nil { return "" } sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]) } func marshalJSON(v any) json.RawMessage { data, err := json.Marshal(v) if err != nil { return json.RawMessage(`{}`) } return data } func bpPtr(bp Blueprint) *Blueprint { if bp.Key == "" { return nil } return &bp } func planPtr2(p *SquadPlan) *WorkflowPlan { wf := p.Workflow return &wf } func answerFor(g GapItem, answer string) string { if answer == "" { return "人工确认按现状继续" } return answer } func appendAnswer(d RespondDecision) string { if strings.TrimSpace(d.Answer) == "" { return "" } return "(" + d.Answer + ")" } func lastCompletedStepID(st *runState) string { id := "" for _, s := range st.Plan.Workflow.Steps { if s.Status == "completed" { id = s.ID } } return id }