package squad import ( "bytes" "context" "encoding/json" "fmt" "log/slog" "os" "path/filepath" "strings" "sync" "time" "chat-agent-system/internal/agent" ) // 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"` // DegradedSteps A5:非关键步骤失败降级后继续的步骤 ID(Success 仍为 true)。 DegradedSteps []string `json:"degraded_steps,omitempty"` } // WorkflowDriver 负责动态工作流调度与沙箱隔离执行 type WorkflowDriver struct { engine *WorkspaceEngine evaluator *StepEvaluator } func NewWorkflowDriver(engine *WorkspaceEngine) *WorkflowDriver { if engine == nil { engine = NewWorkspaceEngine(nil) } 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 := ProgressFromContext(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() defer func() { if rec := recover(); rec != nil { mu.Lock() if groupErr == nil { groupErr = fmt.Errorf("parallel step panic: %v", rec) } mu.Unlock() } }() 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 { if cerr := contextErrString(runCtx); cerr != "" { slog.Warn("squad_parallel_cancelled", "error", groupErr.Error(), "cancel_reason", cerr, "parent_cancel", contextErrString(ctx)) } 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 := ProgressFromContext(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 := ProgressFromContext(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 { // A5 非关键步骤降级:记录 degraded 继续推进,收口统一标 degraded if !step.IsCritical() { markStepDegraded(wsDir, wf, step, err) stepsRun++ _ = d.SaveWorkflow(wsDir, wf) continue } 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 := ProgressFromContext(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) // 收口:A5 存在降级步骤时整体状态 degraded(非 completed 也非 failed) finalStatus := "completed" var degradedSteps []string for _, s := range wf.Steps { if s.Status == "degraded" { finalStatus = "degraded" degradedSteps = append(degradedSteps, s.ID) } } wf.Status = finalStatus _ = d.SaveWorkflow(wsDir, wf) _ = d.updateWorkspaceStatus(wsDir, finalStatus) 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, DegradedSteps: degradedSteps, }, 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 } } } } // markStepDegraded A5:非关键步骤失败降级——状态落 degraded、记录错误、轨迹审计。 func markStepDegraded(wsDir string, wf *WorkflowPlan, step StepSpec, err error) []string { for i := range wf.Steps { if wf.Steps[i].ID == step.ID { wf.Steps[i].Status = "degraded" wf.Steps[i].Error = err.Error() } } appendGovAudit(wsDir, "step_degraded", map[string]string{ "step_id": step.ID, "error": err.Error(), "kind": "non_critical", }) return nil } 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", }) } sink := ProgressFromContext(ctx) roleTitle := FriendlyRoleName(step.Role) if sink != nil { sink.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} // fields.json is requested in TASK.md for organizer, but must not be a hard // runner target: models often emit only output.md when materials are thin, // and that used to fail closed as MISSING_TARGET / kill FullNone nightly. // 治理:执行前重复识别——若历史轨迹显示该步骤已连续两次无进度重复,强制终止 if mu != nil { mu.Lock() } histEntries := LoadTrace(wsDir) if mu != nil { mu.Unlock() } if verdict := DetectRepetition(histEntries, step.ID); verdict.Detected { if st := ProgressFromContext(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, } // 过程流桥:把成员执行的思考/工具调用增量翻译为 detail 事件(sink 缺失时为 no-op)。 // 注意 retry 循环内多次 Execute 共用一个 bridge 语义(同 step 聚合),Stop 只在首次调用。 stopProcessBridge := AttachProcessBridge(sink, step, &req) var ( res *agent.Result execErr error matErr error duration int64 ok bool lastAssess agent.FailureAssessment basePrompt = string(taskData) ) defer stopProcessBridge() 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 } req.Prompt = basePrompt if attempt > 1 && strings.Contains(lastAssess.Reason, "settled-no-deliverable") { req.Prompt = basePrompt + "\n\n【纠偏重试】你上一轮没有调用 write 工具写 " + deliverableName + ",本轮必须先用 write 工具把完整交付物写入当前目录 " + deliverableName + ",再结束。" } if attempt > 1 && sink != nil { sink.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) } recoveredFromDisk := false if strings.TrimSpace(newContent) == "" { if again, rerr := os.ReadFile(prevOutPath); rerr == nil && deliverableWroteThisInvocation(prevContent, again) { newContent = string(again) wroteThisInvocation = true recoveredFromDisk = true } } ok = stepOutputAccepted(execErr, matErr, wroteThisInvocation, newContent, res) if !ok || recoveredFromDisk { slog.Info("squad_step_output_judge", "step_id", step.ID, "exec_err", errString(execErr), "res_success", res != nil && res.Success, "res_error_reason", resultErrorReason(res), "has_trusted_patch", hasTrustedPatch, "wrote_this_invocation", wroteThisInvocation, "mat_err", errString(matErr), "recovered_from_disk", recoveredFromDisk, "accepted", ok, ) } 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, ExecErr: errString(execErr), ResultSuccess: res != nil && res.Success, ResultError: resultErrorReason(res), HasTrustedPatch: hasTrustedPatch, WroteThisInvocation: wroteThisInvocation, MatErr: errString(matErr), RecoveredFromDisk: strings.TrimSpace(newContent) != "" && !(res != nil && res.Success), CancelReason: contextErrString(ctx), }) 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 } lastAssess = 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 lastAssess.Class != agent.FailureTransient || attempt >= agent.MaxTransientStepInvocations { break } } if !ok && execErr != nil { if sink != nil { sink.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 !ok && res != nil && !res.Success { errMsg := res.ErrorReason if sink != nil { sink.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 sink != nil { sink.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 { sink.EmitDetail(DetailEvent{ Category: "step_output", StepID: step.ID, Role: step.Role, Title: fmt.Sprintf("%s阶段性产出: %s", roleTitle, step.Title), Content: string(stepData), Collapsible: true, }) } } if notify != nil { notify(StepEvent{ StepID: step.ID, Role: step.Role, Title: step.Title, Mode: step.Mode, Status: "completed", DurationMs: duration, }) } return nil } // stepOutputAccepted 以本轮写盘产物为事实源。runner 把 Success 标成 false // (例如 typed result 没带回 patch)但文件已经写好时,不让这一处否决步骤。 func stepOutputAccepted(execErr, matErr error, wrote bool, newContent string, res *agent.Result) bool { if execErr != nil || matErr != nil { return false } if res != nil && res.Success { return true } if !wrote || strings.TrimSpace(newContent) == "" { return false } // 连接中断或可重试失败留下的文件可能是截断的,不能当成交付成功。 // 交付物缺失/空结果但盘上已有正文时,以文件为准。 if res != nil && (res.Retryable || hardRunnerFailure(res.ErrorReason)) { return false } return true } func hardRunnerFailure(reason string) bool { reason = strings.ToLower(strings.TrimSpace(reason)) if reason == "" || strings.Contains(reason, "missing target deliverable") { return false } return true } func errString(err error) string { if err == nil { return "" } return err.Error() } func resultErrorReason(res *agent.Result) string { if res == nil { return "" } return res.ErrorReason } func contextErrString(ctx context.Context) string { if ctx == nil || ctx.Err() == nil { return "" } return ctx.Err().Error() } 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 sink := ProgressFromContext(ctx) if sink != nil { sink.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) }