package squad import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "time" "chat-agent-system/internal/agent" "chat-agent-system/internal/stream" ) // StepEvent 表示小队单步执行生命周期的事件 type StepEvent struct { StepID string `json:"step_id"` Role string `json:"role"` Title string `json:"title"` Mode string `json:"mode"` Status string `json:"status"` // started, completed, failed, skipped DurationMs int64 `json:"duration_ms,omitempty"` Error string `json:"error,omitempty"` } // ExecuteOptions 执行调度选项 type ExecuteOptions struct { Timeout time.Duration StepNotify func(StepEvent) // BudgetPolicy 治理预算策略覆盖(缺省 DefaultBudgetPolicy;A2/A3 任务档位可收紧) BudgetPolicy *BudgetPolicy } // WorkflowExecutionResult 工作流执行结果 type WorkflowExecutionResult struct { Success bool `json:"success"` StepsRun int `json:"steps_run"` Deliverable string `json:"deliverable"` FinalContent []byte `json:"-"` Duration time.Duration `json:"duration"` Fields map[string]string `json:"fields,omitempty"` ErrorReason string `json:"error_reason,omitempty"` } // WorkflowDriver 负责动态工作流调度与沙箱隔离执行 type WorkflowDriver struct { engine *WorkspaceEngine evaluator *StepEvaluator } func NewWorkflowDriver(engine *WorkspaceEngine) *WorkflowDriver { if engine == nil { engine = NewWorkspaceEngine() } return &WorkflowDriver{engine: engine, evaluator: NewStepEvaluator()} } // SetEvaluator 配置自定义单步评估器 func (d *WorkflowDriver) SetEvaluator(ev *StepEvaluator) { d.evaluator = ev } // LoadWorkflow 读取工作区 workflow.json func (d *WorkflowDriver) LoadWorkflow(wsDir string) (*WorkflowPlan, error) { p := filepath.Join(wsDir, "workflow.json") data, err := os.ReadFile(p) if err != nil { return nil, fmt.Errorf("read workflow.json: %w", err) } var wf WorkflowPlan if err := json.Unmarshal(data, &wf); err != nil { return nil, fmt.Errorf("unmarshal workflow.json: %w", err) } return &wf, nil } // SaveWorkflow 持久化 workflow.json func (d *WorkflowDriver) SaveWorkflow(wsDir string, wf *WorkflowPlan) error { p := filepath.Join(wsDir, "workflow.json") return writeJSON(p, wf) } // LoadBlackboard 读取工作区 blackboard.json func (d *WorkflowDriver) LoadBlackboard(wsDir string) (*BlackboardState, error) { p := filepath.Join(wsDir, "blackboard.json") data, err := os.ReadFile(p) if err != nil { return nil, fmt.Errorf("read blackboard.json: %w", err) } var bb BlackboardState if err := json.Unmarshal(data, &bb); err != nil { return nil, fmt.Errorf("unmarshal blackboard.json: %w", err) } return &bb, nil } // SaveBlackboard 保存 blackboard.json func (d *WorkflowDriver) SaveBlackboard(wsDir string, bb *BlackboardState) error { bb.UpdatedAt = time.Now() p := filepath.Join(wsDir, "blackboard.json") return writeJSON(p, bb) } // AddStep 动态向工作流追加任务步骤并自动构建其子工作区 func (d *WorkflowDriver) AddStep(wsDir string, step StepSpec) error { wf, err := d.LoadWorkflow(wsDir) if err != nil { return err } if step.SubWorkspace == "" { step.SubWorkspace = fmt.Sprintf("tasks/%02d_%s", len(wf.Steps)+1, step.Role) } if step.Status == "" { step.Status = "pending" } if step.Mode == "" { step.Mode = "execute" } // 准备子任务沙箱 squadCfg, _ := d.loadSquadConfig(wsDir) tmpl, _ := os.ReadFile(filepath.Join(wsDir, "template.md")) singleStepPlan := WorkflowPlan{Steps: []StepSpec{step}} if err := d.engine.buildSubWorkspaces(wsDir, singleStepPlan, squadCfg, string(tmpl)); err != nil { return fmt.Errorf("build added sub workspace: %w", err) } wf.Steps = append(wf.Steps, step) return d.SaveWorkflow(wsDir, wf) } // RemoveStep 动态删除未执行的任务步骤 func (d *WorkflowDriver) RemoveStep(wsDir string, stepID string) error { wf, err := d.LoadWorkflow(wsDir) if err != nil { return err } idx := -1 for i, s := range wf.Steps { if s.ID == stepID { idx = i break } } if idx == -1 { return fmt.Errorf("step %s not found in workflow", stepID) } wf.Steps = append(wf.Steps[:idx], wf.Steps[idx+1:]...) return d.SaveWorkflow(wsDir, wf) } // UpdateStep 动态修改现有任务步骤(如重写验收标准或切换模式) func (d *WorkflowDriver) UpdateStep(wsDir string, step StepSpec) error { wf, err := d.LoadWorkflow(wsDir) if err != nil { return err } found := false for i, s := range wf.Steps { if s.ID == step.ID { wf.Steps[i] = step found = true break } } if !found { return fmt.Errorf("step %s not found to update", step.ID) } // 同步更新子工作区 TASK.md squadCfg, _ := d.loadSquadConfig(wsDir) tmpl, _ := os.ReadFile(filepath.Join(wsDir, "template.md")) _ = d.engine.buildSubWorkspaces(wsDir, WorkflowPlan{Steps: []StepSpec{step}}, squadCfg, string(tmpl)) return d.SaveWorkflow(wsDir, wf) } // Execute 调度并执行工作区内的全套步骤 func (d *WorkflowDriver) Execute(ctx context.Context, wsDir string, backend agent.Backend, opts ExecuteOptions) (*WorkflowExecutionResult, error) { start := time.Now() wf, err := d.LoadWorkflow(wsDir) if err != nil { return nil, err } wf.Status = "in_progress" _ = d.SaveWorkflow(wsDir, wf) _ = d.updateWorkspaceStatus(wsDir, "running") timeout := opts.Timeout if timeout <= 0 { timeout = 30 * time.Minute } runCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() var mu sync.Mutex stepsRun := 0 var producedFiles []string // 治理内核:加载预算账本(Execute 前检查、后入账;含软硬两级阈值) govPolicy := DefaultBudgetPolicy() if opts.BudgetPolicy != nil { govPolicy = *opts.BudgetPolicy } ledger := LoadOrCreateLedger(wsDir, govPolicy) defer func() { _ = ledger.Save(wsDir) }() i := 0 for i < len(wf.Steps) { step := wf.Steps[i] if step.Status == "skipped" { i++ continue } // 治理:已归档锁定的步骤跳过重跑(完成即冻结;显式 ReopenStep 后方可重新执行) if step.Frozen { i++ continue } if step.Parallel { var parallelGroup []StepSpec for i < len(wf.Steps) && wf.Steps[i].Parallel { if wf.Steps[i].Status != "skipped" && !wf.Steps[i].Frozen { parallelGroup = append(parallelGroup, wf.Steps[i]) } i++ } if len(parallelGroup) > 0 { // 治理:并行组逐步预扣预算,超限即硬停(partial output 已由归档机制保留) for range parallelGroup { if rErr := ledger.ReserveStep(); rErr != nil { _ = ledger.Save(wsDir) if st := stream.FromContext(ctx); st != nil { st.EmitPhase("governance", "预算硬停: "+rErr.Error(), "taskgov", 0, 0, 0) } wf.Status = "failed" _ = d.SaveWorkflow(wsDir, wf) _ = d.updateWorkspaceStatus(wsDir, "failed") return &WorkflowExecutionResult{ Success: false, ErrorReason: "预算硬停: " + rErr.Error(), Duration: time.Since(start), }, rErr } } for _, pg := range parallelGroup { markStepExecuting(wf, pg.ID) } var wg sync.WaitGroup var groupErr error for _, s := range parallelGroup { sCopy := s wg.Add(1) go func() { defer wg.Done() if err := d.runOneStep(runCtx, wsDir, sCopy, backend, timeout, opts.StepNotify, &mu, &producedFiles, ledger); err != nil { mu.Lock() if groupErr == nil { groupErr = err } mu.Unlock() } }() } wg.Wait() if groupErr != nil { wf.Status = "failed" _ = d.SaveWorkflow(wsDir, wf) _ = d.updateWorkspaceStatus(wsDir, "failed") return &WorkflowExecutionResult{ Success: false, ErrorReason: groupErr.Error(), Duration: time.Since(start), }, groupErr } stepsRun += len(parallelGroup) // 治理:FSM 受控迁移 + 步骤归档锁定 + 软线提醒 for _, pg := range parallelGroup { if tErr := TransitionStep(wf, pg.ID, StepStateCompleted); tErr == nil { now := time.Now() for idx := range wf.Steps { if wf.Steps[idx].ID == pg.ID { if _, fErr := FreezeStep(wsDir, pg); fErr == nil { wf.Steps[idx].Frozen = true wf.Steps[idx].ArchivedAt = &now } } } } } if msg := ledger.CheckSoftLimit(); msg != "" { if st := stream.FromContext(ctx); st != nil { st.EmitPhase("governance", msg, "taskgov", 0, 0, 0) } } _ = d.SaveWorkflow(wsDir, wf) } } else { stepIndex := i i++ // 治理:预算预扣(超限硬停) if rErr := ledger.ReserveStep(); rErr != nil { _ = ledger.Save(wsDir) if st := stream.FromContext(ctx); st != nil { st.EmitPhase("governance", "预算硬停: "+rErr.Error(), "taskgov", 0, 0, 0) } wf.Status = "failed" _ = d.SaveWorkflow(wsDir, wf) _ = d.updateWorkspaceStatus(wsDir, "failed") return &WorkflowExecutionResult{ Success: false, ErrorReason: "预算硬停: " + rErr.Error(), Duration: time.Since(start), }, rErr } // 治理:受控迁移至执行态(legacy 重跑场景直接放行) markStepExecuting(wf, step.ID) if err := d.runOneStep(runCtx, wsDir, step, backend, timeout, opts.StepNotify, &mu, &producedFiles, ledger); err != nil { wf.Status = "failed" _ = d.SaveWorkflow(wsDir, wf) _ = d.updateWorkspaceStatus(wsDir, "failed") return &WorkflowExecutionResult{ Success: false, ErrorReason: err.Error(), Duration: time.Since(start), }, err } stepsRun++ // 治理:FSM 受控迁移 + 步骤归档锁定 + 软线提醒 if tErr := TransitionStep(wf, step.ID, StepStateCompleted); tErr == nil { if _, fErr := FreezeStep(wsDir, step); fErr == nil { now := time.Now() wf.Steps[stepIndex].Frozen = true wf.Steps[stepIndex].ArchivedAt = &now } } if msg := ledger.CheckSoftLimit(); msg != "" { if st := stream.FromContext(ctx); st != nil { st.EmitPhase("governance", msg, "taskgov", 0, 0, 0) } } _ = d.SaveWorkflow(wsDir, wf) if step.AllowDynamicAdjustment && d.evaluator != nil { d.applyDynamicAdjustment(ctx, wsDir, wf, step, &mu, opts.StepNotify) } } } // 收集最终交付物并汇总到工作区根目录 output.md finalContent, _ := d.resolveFinalDeliverable(wsDir, wf) if len(finalContent) == 0 { // 容错:若未提取到内容,读取 template.md finalContent, _ = os.ReadFile(filepath.Join(wsDir, "template.md")) } rootOut := filepath.Join(wsDir, "output.md") _ = os.WriteFile(rootOut, finalContent, 0o644) wf.Status = "completed" _ = d.SaveWorkflow(wsDir, wf) _ = d.updateWorkspaceStatus(wsDir, "completed") bb, _ := d.LoadBlackboard(wsDir) var finalFields map[string]string if bb != nil { finalFields = bb.Fields } return &WorkflowExecutionResult{ Success: true, StepsRun: stepsRun, Deliverable: rootOut, FinalContent: finalContent, Duration: time.Since(start), Fields: finalFields, }, nil } // markStepExecuting 受控迁移至执行态;对 legacy 重跑场景(如 completed 未冻结的 // 历史工作区再次 Execute)迁移表不放行时直接落位并保留轨迹审计语义 func markStepExecuting(wf *WorkflowPlan, stepID string) { if err := TransitionStep(wf, stepID, StepStateExecuting); err != nil { for i := range wf.Steps { if wf.Steps[i].ID == stepID { wf.Steps[i].Status = StepStateExecuting } } } } func (d *WorkflowDriver) runOneStep( ctx context.Context, wsDir string, step StepSpec, backend agent.Backend, stepTimeout time.Duration, notify func(StepEvent), mu *sync.Mutex, producedFiles *[]string, ledger *BudgetLedger, ) error { if mu == nil { var localMu sync.Mutex mu = &localMu } if producedFiles == nil { var localFiles []string producedFiles = &localFiles } // 治理:归档锁定守卫——冻结步骤禁止隐式重跑,必须显式 ReopenStep if step.Frozen { return fmt.Errorf("治理拦截: 步骤 %s 已归档锁定(完成即冻结),如需重跑请先显式重开", step.ID) } subDir := filepath.Join(wsDir, filepath.FromSlash(step.SubWorkspace)) _ = os.MkdirAll(subDir, 0o755) deliverableName := "output.md" if step.Mode == "plan" || step.Mode == "replan" { deliverableName = "output_plan.md" } else if step.Mode == "review_plan" { deliverableName = "review_notes.md" } if notify != nil { notify(StepEvent{ StepID: step.ID, Role: step.Role, Title: step.Title, Mode: step.Mode, Status: "started", }) } streamer := stream.FromContext(ctx) roleTitle := FriendlyRoleName(step.Role) if streamer != nil { streamer.EmitPhase("step_executing", fmt.Sprintf("%s正在进行:%s...", roleTitle, step.Title), step.Role, 0, 0, 0) } stepStart := time.Now() // 若当前步骤为 leader 审核定稿,且依赖前序专员稿件,先预置 baseline 草稿,供审查与润色 if step.Role == "leader" && deliverableName == "output.md" { targetFile := filepath.Join(subDir, deliverableName) if _, err := os.Stat(targetFile); os.IsNotExist(err) && len(step.Needs) > 0 { srcFile := filepath.Join(wsDir, filepath.FromSlash(step.Needs[0])) if srcData, err := os.ReadFile(srcFile); err == nil && len(srcData) > 0 { _ = os.WriteFile(targetFile, srcData, 0o644) } } } // 1. 读取 TASK.md 与 AGENTS.md taskData, _ := os.ReadFile(filepath.Join(subDir, "TASK.md")) agentsData, _ := os.ReadFile(filepath.Join(subDir, "AGENTS.md")) targets := []string{deliverableName} if step.Role == "organizer" { targets = append(targets, "fields.json") } // 治理:执行前重复识别——若历史轨迹显示该步骤已连续两次无进度重复,强制终止 if mu != nil { mu.Lock() } histEntries := LoadTrace(wsDir) if mu != nil { mu.Unlock() } if verdict := DetectRepetition(histEntries, step.ID); verdict.Detected { if st := stream.FromContext(ctx); st != nil { st.EmitPhase("governance", "重复识别强制终止: "+verdict.Reason, "taskgov", 0, 0, 0) } appendGovAudit(wsDir, "loop_aborted", map[string]string{"step_id": step.ID, "kind": verdict.Kind, "reason": verdict.Reason}) return fmt.Errorf("治理强制终止: %s", verdict.Reason) } // 治理:记录本轮执行前的既有产物(用于计算与上一轮的相似度) prevOutPath := filepath.Join(subDir, deliverableName) prevContent := "" if b, rErr := os.ReadFile(prevOutPath); rErr == nil { prevContent = string(b) } if stepTimeout <= 0 { stepTimeout = 30 * time.Minute } req := agent.Request{ WorkDir: subDir, Prompt: string(taskData), Instructions: string(agentsData), Targets: targets, Timeout: stepTimeout, } var ( res *agent.Result execErr error matErr error duration int64 ok bool ) for attempt := 1; attempt <= agent.MaxTransientStepInvocations; attempt++ { onDisk, _ := os.ReadFile(prevOutPath) wroteThisInvocation := deliverableWroteThisInvocation(prevContent, onDisk) if attempt > 1 && wroteThisInvocation { // Preserve a this-invocation file (may be truncated). Do not call again. break } if attempt > 1 && streamer != nil { streamer.EmitPhase( "step_retrying", fmt.Sprintf("%s遇瞬时失败,发起全新步骤调用(%d/%d)", roleTitle, attempt, agent.MaxTransientStepInvocations), step.Role, 0, 0, 0, ) } res, execErr = backend.Execute(ctx, req) duration = time.Since(stepStart).Milliseconds() matErr = nil incoming := map[string][]byte{} if res != nil { incoming = res.Patches } hasTrustedPatch := incomingHasDeliverable(subDir, deliverableName, incoming) if execErr == nil && res != nil && res.Success { if _, matErr = agent.MaterializeTargets(subDir, []string{deliverableName}, incoming); matErr != nil { res.Success = false res.ErrorReason = matErr.Error() } else if !hasTrustedPatch { after, _ := os.ReadFile(prevOutPath) if !deliverableWroteThisInvocation(prevContent, after) { matErr = fmt.Errorf("missing target deliverable %s", deliverableName) res.Success = false res.ErrorReason = matErr.Error() } } } onDisk, _ = os.ReadFile(prevOutPath) wroteThisInvocation = deliverableWroteThisInvocation(prevContent, onDisk) newContent := "" if hasTrustedPatch && res != nil { if b, has := res.Patches[deliverableName]; has { newContent = string(b) } } if newContent == "" && wroteThisInvocation { newContent = string(onDisk) } ok = execErr == nil && res != nil && res.Success && matErr == nil sim := OutputSimilarity(prevContent, newContent) if mu != nil { mu.Lock() } traceSeq := len(LoadTrace(wsDir)) + 1 _ = AppendTrace(wsDir, TraceEntry{ Seq: traceSeq, StepID: step.ID, Role: step.Role, Timestamp: time.Now(), PromptDigest: digestString(string(taskData)), OutputDigest: digestString(newContent), SimilarityToPrev: sim, DurationMs: duration, Success: ok, }) if mu != nil { mu.Unlock() } if ledger != nil { if mu != nil { mu.Lock() } ledger.RecordAgentCall() if mu != nil { mu.Unlock() } } if ok { break } if ctx.Err() != nil { if execErr == nil { execErr = ctx.Err() } break } assess := agent.AssessStepFailure(agent.StepFailureSignals{ Err: execErr, Result: res, WorkDir: subDir, TargetMissing: !wroteThisInvocation && (matErr != nil || (res != nil && strings.Contains(res.ErrorReason, "missing target deliverable"))), TargetExists: wroteThisInvocation, }) if assess.Class != agent.FailureTransient || attempt >= agent.MaxTransientStepInvocations { break } } if execErr != nil { if streamer != nil { streamer.EmitPhase("step_failed", fmt.Sprintf("%s执行异常:%s", roleTitle, execErr.Error()), step.Role, 0, 0, 0) } if notify != nil { notify(StepEvent{ StepID: step.ID, Role: step.Role, Title: step.Title, Mode: step.Mode, Status: "failed", DurationMs: duration, Error: execErr.Error(), }) } return fmt.Errorf("step %s (%s) backend execute: %w", step.ID, step.Role, execErr) } if res != nil && !res.Success { errMsg := res.ErrorReason if streamer != nil { streamer.EmitPhase("step_failed", fmt.Sprintf("%s执行未成功:%s", roleTitle, errMsg), step.Role, 0, 0, 0) } if notify != nil { notify(StepEvent{ StepID: step.ID, Role: step.Role, Title: step.Title, Mode: step.Mode, Status: "failed", DurationMs: duration, Error: errMsg, }) } return fmt.Errorf("step %s failed: %s", step.ID, errMsg) } if !ok { reason := "missing target deliverable " + deliverableName if res != nil && res.ErrorReason != "" { reason = res.ErrorReason } if matErr != nil { reason = matErr.Error() } return fmt.Errorf("step %s %s", step.ID, reason) } targetPath := filepath.Join(subDir, deliverableName) // 3. 检查是否有 fields.json 并合并入 blackboard.json fieldsPath := filepath.Join(subDir, "fields.json") if fData, err := os.ReadFile(fieldsPath); err == nil { var extractedFields map[string]string if err := json.Unmarshal(fData, &extractedFields); err == nil { mu.Lock() if bb, err := d.LoadBlackboard(wsDir); err == nil { if bb.Fields == nil { bb.Fields = make(map[string]string) } for k, v := range extractedFields { bb.Fields[k] = v } _ = d.SaveBlackboard(wsDir, bb) } mu.Unlock() } } // 4. 记录日志 logEntry := fmt.Sprintf("[%s] 步骤 %s (%s) 执行完成,耗时 %dms\n", time.Now().Format(time.RFC3339), step.ID, step.Role, duration) _ = os.WriteFile(filepath.Join(subDir, "execution.log"), []byte(logEntry), 0o644) mu.Lock() *producedFiles = append(*producedFiles, filepath.ToSlash(filepath.Join(step.SubWorkspace, deliverableName))) mu.Unlock() if streamer != nil { streamer.EmitPhase("step_completed", fmt.Sprintf("%s完成任务:%s (耗时 %dms)", roleTitle, step.Title, duration), step.Role, 0, 0, 0) if stepData, err := os.ReadFile(targetPath); err == nil && len(stepData) > 0 { streamer.EmitDetail("step_output", step.ID, step.Role, fmt.Sprintf("%s阶段性产出: %s", roleTitle, step.Title), string(stepData), true) } } if notify != nil { notify(StepEvent{ StepID: step.ID, Role: step.Role, Title: step.Title, Mode: step.Mode, Status: "completed", DurationMs: duration, }) } return nil } func deliverableWroteThisInvocation(baseline string, onDisk []byte) bool { if len(bytes.TrimSpace(onDisk)) == 0 { return false } return string(onDisk) != baseline } func incomingHasDeliverable(workDir, target string, incoming map[string][]byte) bool { for k, v := range incoming { if len(bytes.TrimSpace(v)) == 0 { continue } if agent.MatchDeliverableRel(workDir, k, target) { return true } } return false } func (d *WorkflowDriver) applyDynamicAdjustment( ctx context.Context, wsDir string, wf *WorkflowPlan, step StepSpec, mu *sync.Mutex, notify func(StepEvent), ) { deliverableName := "output.md" if step.Mode == "plan" || step.Mode == "replan" { deliverableName = "output_plan.md" } outPath := filepath.Join(wsDir, filepath.FromSlash(step.SubWorkspace), deliverableName) outContent, _ := os.ReadFile(outPath) bb, _ := d.LoadBlackboard(wsDir) evalRes, err := d.evaluator.EvaluateStepOutput(wsDir, step, string(outContent), bb) if err != nil || evalRes == nil { return } mu.Lock() defer mu.Unlock() // 1. 合并新推断的基准假设 if len(evalRes.NewAssumptions) > 0 && bb != nil { for _, na := range evalRes.NewAssumptions { exists := false for _, ea := range bb.Assumptions { if ea.Topic == na.Topic { exists = true break } } if !exists { bb.Assumptions = append(bb.Assumptions, na) } } _ = d.SaveBlackboard(wsDir, bb) } // 2. 处理动态跳步 if len(evalRes.StepsToSkip) > 0 { for _, skipID := range evalRes.StepsToSkip { target := strings.ToLower(skipID) for idx := range wf.Steps { curID := strings.ToLower(wf.Steps[idx].ID) curRole := strings.ToLower(wf.Steps[idx].Role) isMatch := (curID == target || curRole == target || strings.Contains(curID, target) || strings.Contains(target, curID)) if isMatch && (wf.Steps[idx].Status == "pending" || wf.Steps[idx].Status == "") { wf.Steps[idx].Status = "skipped" if notify != nil { notify(StepEvent{ StepID: wf.Steps[idx].ID, Role: wf.Steps[idx].Role, Title: wf.Steps[idx].Title, Status: "skipped", }) } } } } _ = d.SaveWorkflow(wsDir, wf) } // 3. 处理动态加步 if len(evalRes.DynamicStepsToAdd) > 0 { squadCfg, _ := d.loadSquadConfig(wsDir) tmpl, _ := os.ReadFile(filepath.Join(wsDir, "template.md")) for _, addStep := range evalRes.DynamicStepsToAdd { alreadyExists := false for _, s := range wf.Steps { if s.ID == addStep.ID { alreadyExists = true break } } if alreadyExists { continue } if addStep.SubWorkspace == "" { addStep.SubWorkspace = fmt.Sprintf("tasks/%02d_%s", len(wf.Steps)+1, addStep.Role) } if addStep.Status == "" { addStep.Status = "pending" } if addStep.Mode == "" { addStep.Mode = "execute" } // 构建子沙箱 _ = d.engine.buildSubWorkspaces(wsDir, WorkflowPlan{Steps: []StepSpec{addStep}}, squadCfg, string(tmpl)) // 插入在 writer 或 leader 之前,确保专家成果在成文前产生 insertIdx := len(wf.Steps) for idx, s := range wf.Steps { if s.Role == "writer" || s.Role == "leader" { insertIdx = idx break } } newSteps := make([]StepSpec, 0, len(wf.Steps)+1) newSteps = append(newSteps, wf.Steps[:insertIdx]...) newSteps = append(newSteps, addStep) newSteps = append(newSteps, wf.Steps[insertIdx:]...) wf.Steps = newSteps streamer := stream.FromContext(ctx) if streamer != nil { streamer.EmitPhase("dynamic_adjustment", fmt.Sprintf("单步评估发现缺口,动态追加任务步骤:%s (%s)", addStep.Title, FriendlyRoleName(addStep.Role)), addStep.Role, 0, 0, 0) } } _ = d.SaveWorkflow(wsDir, wf) } } func (d *WorkflowDriver) resolveFinalDeliverable(wsDir string, wf *WorkflowPlan) ([]byte, string) { // 查找优先级:leader 的 output.md > writer 的 output.md > 最后一个步骤的 output.md for i := len(wf.Steps) - 1; i >= 0; i-- { step := wf.Steps[i] if step.Role == "leader" && (step.Mode == "execute" || step.Mode == "") { p := filepath.Join(wsDir, filepath.FromSlash(step.SubWorkspace), "output.md") if data, err := os.ReadFile(p); err == nil && len(strings.TrimSpace(string(data))) > 0 { return data, p } } } for i := len(wf.Steps) - 1; i >= 0; i-- { step := wf.Steps[i] if step.Role == "writer" && (step.Mode == "execute" || step.Mode == "") { p := filepath.Join(wsDir, filepath.FromSlash(step.SubWorkspace), "output.md") if data, err := os.ReadFile(p); err == nil && len(strings.TrimSpace(string(data))) > 0 { return data, p } } } for i := len(wf.Steps) - 1; i >= 0; i-- { step := wf.Steps[i] p := filepath.Join(wsDir, filepath.FromSlash(step.SubWorkspace), "output.md") if data, err := os.ReadFile(p); err == nil && len(strings.TrimSpace(string(data))) > 0 { return data, p } } return nil, "" } func (d *WorkflowDriver) loadSquadConfig(wsDir string) (SquadConfig, error) { p := filepath.Join(wsDir, "squad.json") var sc SquadConfig data, err := os.ReadFile(p) if err != nil { return sc, err } err = json.Unmarshal(data, &sc) return sc, err } func (d *WorkflowDriver) updateWorkspaceStatus(wsDir string, status string) error { p := filepath.Join(wsDir, "workspace.json") data, err := os.ReadFile(p) if err != nil { return err } var meta WorkspaceMetadata if err := json.Unmarshal(data, &meta); err != nil { return err } meta.Status = status meta.UpdatedAt = time.Now() if status == "completed" { now := time.Now() meta.CompletedAt = &now } return writeJSON(p, meta) }