feat(squad): 实现单步评估器StepEvaluator与工作流动态加步、跳步及基准假设推断
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+170
-27
@@ -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-- {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user