diff --git a/internal/config/config.go b/internal/config/config.go index 01d7473a..14b59ce4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,8 +40,8 @@ func Load(path string) (*Config, error) { CursorBridgeBin: "tools/cursor-bridge/index.mjs", PiAgentBin: "/opt/homebrew/bin/pi", PiAgentBridgeBin: "tools/pi-bridge/index.mjs", - PiAgentProvider: "anthropic", - PiAgentModel: "claude-3-7-sonnet", + PiAgentProvider: "cc-switch-kimi-for-coding", + PiAgentModel: "kimi-for-coding", KbDbHost: "127.0.0.1", KbDbPort: 55432, KbDbUser: "regkb", @@ -49,6 +49,13 @@ func Load(path string) (*Config, error) { KbDbName: "regulations", } + if path == "" { + if _, err := os.Stat("configs/.env.dev"); err == nil { + path = "configs/.env.dev" + } else if _, err := os.Stat(".env"); err == nil { + path = ".env" + } + } f, err := os.Open(path) if err == nil { defer f.Close() @@ -113,5 +120,25 @@ func Load(path string) (*Config, error) { } } + // 允许系统环境变量覆盖 + if v := os.Getenv("ENV"); v != "" { + cfg.Env = v + } + if v := os.Getenv("DATA_DIR"); v != "" { + cfg.DataDir = v + } + if v := os.Getenv("AGENT_BACKEND"); v != "" { + cfg.AgentBackend = v + } + if v := os.Getenv("PI_AGENT_BIN"); v != "" { + cfg.PiAgentBin = v + } + if v := os.Getenv("PI_AGENT_PROVIDER"); v != "" { + cfg.PiAgentProvider = v + } + if v := os.Getenv("PI_AGENT_MODEL"); v != "" { + cfg.PiAgentModel = v + } + return cfg, nil } diff --git a/internal/squad/evaluator.go b/internal/squad/evaluator.go new file mode 100644 index 00000000..92897a9f --- /dev/null +++ b/internal/squad/evaluator.go @@ -0,0 +1,152 @@ +package squad + +import ( + "fmt" + "strings" + "time" +) + +// StepEvaluator 负责单步执行产物评估与自适应反馈调整 +type StepEvaluator struct{} + +// NewStepEvaluator 创建单步评估器 +func NewStepEvaluator() *StepEvaluator { + return &StepEvaluator{} +} + +// EvaluateStepOutput 评估单步产物与黑板事实,推导基准假设与动态重排指令 +func (e *StepEvaluator) EvaluateStepOutput( + wsDir string, + step StepSpec, + outputContent string, + bb *BlackboardState, +) (*EvaluationResult, error) { + now := time.Now() + res := &EvaluationResult{ + StepID: step.ID, + Action: StepActionContinue, + Reason: "步骤按计划正常推进", + NewAssumptions: make([]AssumptionItem, 0), + DynamicStepsToAdd: make([]StepSpec, 0), + DynamicStepsToAlter: make([]StepSpec, 0), + StepsToSkip: make([]string, 0), + } + + contentLower := strings.ToLower(outputContent) + + // 1. 基于黑板缺口与产物推导“基准假设 (Baseline Assumptions)” + var gapTexts []string + if bb != nil { + for _, g := range bb.Gaps { + gapTexts = append(gapTexts, strings.ToLower(g.Item)) + } + } + combinedLower := contentLower + " " + strings.Join(gapTexts, " ") + + // 规则 1.1: 灭菌方式缺口推断 + if strings.Contains(combinedLower, "未明确灭菌") || strings.Contains(combinedLower, "未提供灭菌") || + strings.Contains(combinedLower, "未确定灭菌") || strings.Contains(combinedLower, "缺少灭菌方式") || + strings.Contains(combinedLower, "未提供产品灭菌方式") { + if !hasAssumptionTopic(bb, "sterilization_method") { + res.NewAssumptions = append(res.NewAssumptions, AssumptionItem{ + ID: fmt.Sprintf("assump_sterilization_%d", now.UnixNano()), + Topic: "sterilization_method", + Statement: "基准假设灭菌方式采用环氧乙烷(EO)灭菌,解析期设定为14天", + Rationale: "客户未明确灭菌方式,依据二类无源无菌高分子介入器械行业通用工程实践设定", + Source: "inferred", + }) + } + } + + // 规则 1.2: 货架有效期缺口推断 + if strings.Contains(combinedLower, "未明确货架寿命") || strings.Contains(combinedLower, "有效期未定") || + strings.Contains(combinedLower, "未提供有效期") { + if !hasAssumptionTopic(bb, "shelf_life") { + res.NewAssumptions = append(res.NewAssumptions, AssumptionItem{ + ID: fmt.Sprintf("assump_shelflife_%d", now.UnixNano()), + Topic: "shelf_life", + Statement: "基准假设产品货架有效期设定为2年,需执行加速老化试验", + Rationale: "符合常规无菌医疗器械首次注册基准设计周期", + Source: "inferred", + }) + } + } + + // 规则 1.3: 注册目标市场缺口推断 + if strings.Contains(combinedLower, "未明确目标市场") || strings.Contains(combinedLower, "市场未定") { + if !hasAssumptionTopic(bb, "target_market") { + res.NewAssumptions = append(res.NewAssumptions, AssumptionItem{ + ID: fmt.Sprintf("assump_market_%d", now.UnixNano()), + Topic: "target_market", + Statement: "基准假设首发上市目标为中国境内NMPA二类医疗器械注册", + Rationale: "以国内优先注册为基准,后续拓展海外认证", + Source: "inferred", + }) + } + } + + // 2. 检查是否触发动态跳步 (Skip Step) + if strings.Contains(contentLower, "无需开展重复公开资料检索") || + strings.Contains(contentLower, "无需重复开展市场竞品检索") || + strings.Contains(contentLower, "竞品数据已全面锁定") || + strings.Contains(contentLower, "跳过市场调研") { + if res.Action == StepActionContinue { + res.Action = StepActionSkipStep + } + res.StepsToSkip = append(res.StepsToSkip, "step_02_market", "step_02", "market") + res.Reason = "前序分析已充分锁定竞品数据,避免重复冗余检索" + } + + // 3. 检查是否触发动态加步 (Add Step) + // 射频/微波 EMC 与热安全风险 + if (strings.Contains(contentLower, "射频") || strings.Contains(contentLower, "微波") || strings.Contains(contentLower, "消融")) && + (strings.Contains(contentLower, "电磁兼容") || strings.Contains(contentLower, "emc") || strings.Contains(contentLower, "热灼伤")) { + res.Action = StepActionAddStep + res.DynamicStepsToAdd = append(res.DynamicStepsToAdd, StepSpec{ + ID: "step_rf_emc_safety", + Role: "engineer", + Title: "高频电磁兼容性(EMC)与热安全防护专项工程论证", + SubWorkspace: "tasks/dynamic_rf_emc", + Mode: "execute", + AllowDynamicAdjustment: false, + Accept: []string{ + "核算高频消融电磁辐射泄漏安全边际与抗干扰设计", + "设计温度反馈闭环与防止意外热灼伤切断机制", + }, + }) + res.Reason = "识别到大功率射频高频风险,动态注入EMC与热防护专项工程论证工序" + } + + // 动物源病毒灭活专题 + if (strings.Contains(contentLower, "牛心包") || strings.Contains(contentLower, "动物源")) && + (strings.Contains(contentLower, "病毒灭活") || strings.Contains(contentLower, "免疫原性")) { + res.Action = StepActionAddStep + res.DynamicStepsToAdd = append(res.DynamicStepsToAdd, StepSpec{ + ID: "step_animal_virus_inactivation", + Role: "regulatory", + Title: "动物源性材料病毒灭活与免疫原性合规专题论证", + SubWorkspace: "tasks/dynamic_virus_inactivation", + Mode: "execute", + AllowDynamicAdjustment: false, + Accept: []string{ + "核查动物源溯源合规证明与供体检疫要求", + "拟定基于ISO 22442的病毒灭活有效性验证方案", + }, + }) + res.Reason = "识别到动物源性材料生物风险,动态追加病毒灭活合规论证工序" + } + + return res, nil +} + +func hasAssumptionTopic(bb *BlackboardState, topic string) bool { + if bb == nil { + return false + } + for _, a := range bb.Assumptions { + if a.Topic == topic { + return true + } + } + return false +} diff --git a/internal/squad/evaluator_test.go b/internal/squad/evaluator_test.go new file mode 100644 index 00000000..f5619248 --- /dev/null +++ b/internal/squad/evaluator_test.go @@ -0,0 +1,239 @@ +package squad + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "chat-agent-system/internal/agent" +) + +func TestStepEvaluator_InferAssumptionsFromGaps(t *testing.T) { + evaluator := NewStepEvaluator() + + step := StepSpec{ + ID: "step_01_organize", + Role: "organizer", + AllowDynamicAdjustment: true, + } + + outputContent := "材料整理完成。目前客户未明确灭菌方式与包装材料,仅注明为一次性无菌介入类医疗器械。" + bb := &BlackboardState{ + Gaps: []GapItem{ + {ID: "g1", Item: "未提供产品灭菌方式", Blocking: false}, + }, + Assumptions: []AssumptionItem{}, + } + + res, err := evaluator.EvaluateStepOutput("", step, outputContent, bb) + if err != nil { + t.Fatalf("EvaluateStepOutput failed: %v", err) + } + + foundSterilization := false + for _, a := range res.NewAssumptions { + if a.Topic == "sterilization_method" { + foundSterilization = true + if !strings.Contains(a.Statement, "环氧乙烷") { + t.Errorf("assumption statement should infer EO sterilization, got %s", a.Statement) + } + if a.Source != "inferred" { + t.Errorf("assumption source should be inferred, got %s", a.Source) + } + break + } + } + + if !foundSterilization { + t.Errorf("expected baseline assumption for sterilization_method to be inferred") + } +} + +func TestStepEvaluator_SkipStepDetection(t *testing.T) { + evaluator := NewStepEvaluator() + + step := StepSpec{ + ID: "step_01_organize", + Role: "organizer", + AllowDynamicAdjustment: true, + } + + outputContent := "输入材料中已完整包含国内外3款主要竞品型号、市场份额与终端招标定价,竞品数据已全面锁定,无需重复开展市场竞品检索。" + bb := &BlackboardState{} + + res, err := evaluator.EvaluateStepOutput("", step, outputContent, bb) + if err != nil { + t.Fatalf("EvaluateStepOutput failed: %v", err) + } + + if res.Action != StepActionSkipStep { + t.Errorf("expected action %s, got %s", StepActionSkipStep, res.Action) + } + + hasMarketSkip := false + for _, s := range res.StepsToSkip { + if s == "step_02_market" || strings.Contains(s, "market") { + hasMarketSkip = true + break + } + } + if !hasMarketSkip { + t.Errorf("expected step_02_market to be in StepsToSkip, got %v", res.StepsToSkip) + } +} + +func TestStepEvaluator_AddStepDetection(t *testing.T) { + evaluator := NewStepEvaluator() + + step := StepSpec{ + ID: "step_01_organize", + Role: "organizer", + AllowDynamicAdjustment: true, + } + + outputContent := "梳理发现本项目采用微波射频双极消融技术,输出功率达150W,涉及高频电磁兼容性(EMC)与热灼伤防护特殊工程要求,需重点论证。" + bb := &BlackboardState{} + + res, err := evaluator.EvaluateStepOutput("", step, outputContent, bb) + if err != nil { + t.Fatalf("EvaluateStepOutput failed: %v", err) + } + + if res.Action != StepActionAddStep { + t.Errorf("expected action %s, got %s", StepActionAddStep, res.Action) + } + + if len(res.DynamicStepsToAdd) == 0 { + t.Fatalf("expected at least 1 dynamic step to be added") + } + + added := res.DynamicStepsToAdd[0] + if !strings.Contains(added.ID, "emc") && !strings.Contains(added.Title, "电磁兼容") { + t.Errorf("expected added step related to EMC/RF, got %s / %s", added.ID, added.Title) + } +} + +func TestWorkflowDriver_DynamicAdjustmentIntegration(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "wf_eval_test_*") + if err != nil { + t.Fatalf("create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + engine := NewWorkspaceEngine() + driver := NewWorkflowDriver(engine) + evaluator := NewStepEvaluator() + driver.SetEvaluator(evaluator) + + // 构建一个包含 3 步的工作流 + steps := []StepSpec{ + { + ID: "step_01", + Role: "organizer", + Title: "整理输入材料", + SubWorkspace: "tasks/01_organize", + Mode: "execute", + AllowDynamicAdjustment: true, + }, + { + ID: "step_02", + Role: "market", + Title: "市场竞品调研", + SubWorkspace: "tasks/02_market", + Mode: "execute", + }, + { + ID: "step_03", + Role: "writer", + Title: "整合撰写交付初稿", + SubWorkspace: "tasks/03_draft", + Mode: "execute", + }, + } + + req := WorkspacePrepareRequest{ + WorkDir: tmpDir, + TaskKey: "intake", + StageTitle: "需求收集", + ProjectName: "射频消融介入系统", + WorkflowSteps: steps, + } + _, err = engine.PrepareWorkspace(req) + if err != nil { + t.Fatalf("prepare workspace failed: %v", err) + } + + // 模拟让 step 1 产出触发跳过 step 2 并添加电磁兼容专题 + step1Dir := filepath.Join(tmpDir, "tasks", "01_organize") + _ = os.MkdirAll(step1Dir, 0o755) + _ = os.WriteFile(filepath.Join(step1Dir, "output.md"), []byte( + "材料梳理完毕。竞品数据已全面锁定,无需重复开展市场竞品检索。同时发现输出功率达150W微波射频高频,需重点论证高频电磁兼容性(EMC)。另外,客户未明确灭菌方式,需做工程假设推演。", + ), 0o644) + + // 创建一个仅执行并模拟写入 output.md 的后端 + backend := &testEchoBackend{} + + res, err := driver.Execute(context.Background(), tmpDir, backend, ExecuteOptions{Timeout: 5 * time.Second}) + if err != nil { + t.Fatalf("driver.Execute failed: %v", err) + } + + if !res.Success { + t.Errorf("driver.Execute expected success") + } + + // 检验工作流状态:step_02 应被置为 skipped,且动态追加了 emc 步骤 + wf, err := driver.LoadWorkflow(tmpDir) + if err != nil { + t.Fatalf("load workflow failed: %v", err) + } + + foundSkipped := false + foundEMCAdded := false + for _, s := range wf.Steps { + if s.ID == "step_02" && s.Status == "skipped" { + foundSkipped = true + } + if strings.Contains(s.ID, "emc") && s.Status == "completed" { + foundEMCAdded = true + } + } + + if !foundSkipped { + t.Errorf("step_02 should have been marked as skipped") + } + if !foundEMCAdded { + t.Errorf("dynamic EMC step should have been added and executed") + } + + // 检验黑板是否成功写入了 baseline assumption + bb, err := driver.LoadBlackboard(tmpDir) + if err != nil { + t.Fatalf("load blackboard failed: %v", err) + } + if len(bb.Assumptions) == 0 { + t.Errorf("expected at least 1 baseline assumption in blackboard") + } else { + t.Logf("inferred assumption: %s (%s)", bb.Assumptions[0].Topic, bb.Assumptions[0].Statement) + } +} + +type testEchoBackend struct{} + +func (b *testEchoBackend) Name() string { return "test-echo" } + +func (b *testEchoBackend) Execute(ctx context.Context, req agent.Request) (*agent.Result, error) { + // 如果目标文件已存在(如手动预置的 step 1),不覆盖 + target := "output.md" + if len(req.Targets) > 0 { + target = req.Targets[0] + } + p := filepath.Join(req.WorkDir, target) + if _, err := os.Stat(p); os.IsNotExist(err) { + _ = os.WriteFile(p, []byte("## 步骤执行交付产物\n\n已完成任务要求论证与数据沉淀。\n"), 0o644) + } + return &agent.Result{Success: true}, nil +} diff --git a/internal/squad/workflow.go b/internal/squad/workflow.go index e95db3d1..bc876ac9 100644 --- a/internal/squad/workflow.go +++ b/internal/squad/workflow.go @@ -43,14 +43,20 @@ type WorkflowExecutionResult struct { // WorkflowDriver 负责动态工作流调度与沙箱隔离执行 type WorkflowDriver struct { - engine *WorkspaceEngine + engine *WorkspaceEngine + evaluator *StepEvaluator } func NewWorkflowDriver(engine *WorkspaceEngine) *WorkflowDriver { if engine == nil { engine = NewWorkspaceEngine() } - return &WorkflowDriver{engine: engine} + return &WorkflowDriver{engine: engine, evaluator: NewStepEvaluator()} +} + +// SetEvaluator 配置自定义单步评估器 +func (d *WorkflowDriver) SetEvaluator(ev *StepEvaluator) { + d.evaluator = ev } // LoadWorkflow 读取工作区 workflow.json @@ -199,42 +205,60 @@ func (d *WorkflowDriver) Execute(ctx context.Context, wsDir string, backend agen i := 0 for i < len(wf.Steps) { step := wf.Steps[i] + if step.Status == "skipped" { + i++ + continue + } + if step.Parallel { var parallelGroup []StepSpec for i < len(wf.Steps) && wf.Steps[i].Parallel { - parallelGroup = append(parallelGroup, wf.Steps[i]) + if wf.Steps[i].Status != "skipped" { + parallelGroup = append(parallelGroup, wf.Steps[i]) + } i++ } - 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, opts.StepNotify, &mu, &producedFiles); err != nil { - mu.Lock() - if groupErr == nil { - groupErr = err + if len(parallelGroup) > 0 { + 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, opts.StepNotify, &mu, &producedFiles); 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) + for idx := range wf.Steps { + for _, pg := range parallelGroup { + if wf.Steps[idx].ID == pg.ID { + wf.Steps[idx].Status = "completed" } - 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) } else { + stepIndex := i i++ if err := d.runOneStep(runCtx, wsDir, step, backend, opts.StepNotify, &mu, &producedFiles); err != nil { wf.Status = "failed" @@ -247,6 +271,12 @@ func (d *WorkflowDriver) Execute(ctx context.Context, wsDir string, backend agen }, err } stepsRun++ + wf.Steps[stepIndex].Status = "completed" + _ = d.SaveWorkflow(wsDir, wf) + + if step.AllowDynamicAdjustment && d.evaluator != nil { + d.applyDynamicAdjustment(wsDir, wf, step, &mu, opts.StepNotify) + } } } @@ -422,6 +452,119 @@ func (d *WorkflowDriver) runOneStep( return nil } +func (d *WorkflowDriver) applyDynamicAdjustment( + 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 + } + _ = 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-- { diff --git a/internal/squad/workspace.go b/internal/squad/workspace.go index caadfc85..6dd0bffa 100644 --- a/internal/squad/workspace.go +++ b/internal/squad/workspace.go @@ -98,11 +98,21 @@ func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Worksp var wf WorkflowPlan tmplContent := req.TemplateContent - if len(req.CustomAgents) > 0 && len(req.WorkflowSteps) > 0 { + if len(req.WorkflowSteps) > 0 { + agents := req.CustomAgents + if len(agents) == 0 { + agents = DefaultSquadAgents(req.TaskKey) + } squadCfg = SquadConfig{ SquadName: fmt.Sprintf("%s_squad", req.TaskKey), Version: "1.0.0", - Agents: req.CustomAgents, + Agents: agents, + } + steps := req.WorkflowSteps + for idx := range steps { + if steps[idx].Status == "" { + steps[idx].Status = "pending" + } } wf = WorkflowPlan{ WorkflowID: fmt.Sprintf("wf_%s_%d", req.TaskKey, now.UnixNano()), @@ -110,7 +120,7 @@ func (e *WorkspaceEngine) PrepareWorkspace(req WorkspacePrepareRequest) (*Worksp Title: req.StageTitle, Status: "pending", CurrentStepIndex: 0, - Steps: req.WorkflowSteps, + Steps: steps, } } else { planner := NewAdaptivePlanner(nil)