Files
Agent-Squad/squad/workflow.go
T
weichenandCursor bfec3c8930
ci / vet-and-test (push) Failing after 3s
ci / docker-checkout-hostgateway (push) Failing after 3s
Import agent-squad as a standalone repository.
Pin Agent-Native as a submodule so the module builds without the multi-repo sibling path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 12:38:35 +00:00

747 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package squad
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"agent-native/exec"
)
// flowDriver 工作流驱动器:推进 runState 直到终态或 HITL 挂起边界。
type flowDriver struct {
opts engineDeps
}
// advance 执行到边界;返回 (结果, 挂起上下文),二者互斥非 nil(infra 错误时都为 nil 且 err 非 nil)。
func (d *flowDriver) advance(ctx context.Context, st *runState) (res *RunResult, gate *hitlGate, err error) {
wf := st.Workflow()
wf.Status = "in_progress"
st.setWorkflow(wf)
d.emit(st, Event{Type: EvPhase, Text: "开始执行工作流"})
i := 0
for i < len(st.Plan.Workflow.Steps) {
step := st.Plan.Workflow.Steps[i]
switch step.Status {
case "completed", "skipped", "failed":
i++
continue
}
// HITL:角色权限升级(bash=ask 且本运行未授权过该角色)。
role, _ := findAgent(st.Plan.Squad.Agents, step.Role)
if role.BashPermission == "ask" && !st.Approved["role:"+role.Role] {
switch d.policy(st).mode() {
case HitlDisabled, HitlAuto:
step.Error = "permission denied: role " + role.Role + " requires bash approval (hitl mode=" + string(d.policy(st).mode()) + ")"
st.Plan.Workflow.Steps[i] = step
return d.failStep(ctx, st, i)
default:
return d.hold(st, HitlPermission, step.ID, step.Role,
fmt.Sprintf("角色 %s(%s)声明 bash_permission=ask,是否授权其在本次运行中使用更高权限?", role.Name, role.Role),
[]HumanOption{
{Value: "approve", Label: "授权本次运行"},
{Value: "reject", Label: "拒绝,步骤失败"},
{Value: "abort", Label: "中止运行"},
},
fmt.Sprintf("步骤:%s\n角色:%s\n禁忌:%v", step.Title, role.Role, role.ForbiddenActions), nil)
}
}
// HITL:步骤显式要求审批。
if step.RequiresApproval && !st.Approved["step:"+step.ID] {
switch d.policy(st).mode() {
case HitlDisabled, HitlAuto:
step.Error = "approval required but hitl mode is " + string(d.policy(st).mode())
st.Plan.Workflow.Steps[i] = step
return d.failStep(ctx, st, i)
default:
return d.hold(st, HitlConfirmation, step.ID, step.Role,
fmt.Sprintf("步骤「%s」标记为需人工审批后才能执行,是否批准?", step.Title),
[]HumanOption{
{Value: "approve", Label: "批准执行"},
{Value: "skip", Label: "跳过该步骤"},
{Value: "abort", Label: "中止运行"},
},
fmt.Sprintf("角色:%s\n产出:%v", step.Role, step.Targets), nil)
}
}
if step.Parallel {
// 收集相邻并行组,组内并发、组间顺序。
var group []int
for i < len(st.Plan.Workflow.Steps) {
s := st.Plan.Workflow.Steps[i]
if s.Parallel && s.Status != "completed" && s.Status != "skipped" && s.Status != "failed" {
group = append(group, i)
i++
continue
}
break
}
if out, g, err := d.runParallel(ctx, st, group); out != nil || g != nil || err != nil {
if out == nil && g != nil {
out = heldResult(st, g)
}
return out, g, err
}
if g2 := d.checkBlockingGaps(st); g2 != nil {
return heldResult(st, g2), g2, nil
}
continue
}
out, g, err := d.runOneStep(ctx, st, i)
if out != nil || g != nil || err != nil {
if out == nil && g != nil {
out = heldResult(st, g)
}
return out, g, err
}
i++
// 阻塞缺口挂起。
if g2 := d.checkBlockingGaps(st); g2 != nil {
return heldResult(st, g2), g2, nil
}
// 动态调整(仅顺序步骤后,避免并发竞态)。
if st.Plan.Workflow.Steps[i-1].AllowDynamicAdjustment && d.opts.Evaluator != nil {
out, g, err := d.applyDynamicAdjustment(ctx, st, i-1)
if out != nil || g != nil || err != nil {
if out == nil && g != nil {
out = heldResult(st, g)
}
return out, g, err
}
}
}
return d.finalize(ctx, st)
}
// runParallel 并发执行一个并行组;关键步骤失败即终止整个运行。
func (d *flowDriver) runParallel(ctx context.Context, st *runState, idxs []int) (*RunResult, *hitlGate, error) {
var wg sync.WaitGroup
var mu sync.Mutex
failedCritical := -1
for _, idx := range idxs {
wg.Add(1)
go func(idx int) {
defer wg.Done()
out, _, err := d.runOneStep(ctx, st, idx)
mu.Lock()
defer mu.Unlock()
if (out != nil && out.Status == RunFailed) || err != nil {
if st.Plan.Workflow.Steps[idx].IsCritical() && failedCritical < 0 {
failedCritical = idx
}
}
}(idx)
}
wg.Wait()
if failedCritical >= 0 {
return d.failStep(ctx, st, failedCritical)
}
return nil, nil, nil
}
// runOneStep 执行单个步骤(含瞬态重试与交付物校验)。
func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*RunResult, *hitlGate, error) {
step := st.Plan.Workflow.Steps[idx]
role, ok := findAgent(st.Plan.Squad.Agents, step.Role)
if !ok {
if r, exists := d.opts.Registry.Get(step.Role); exists {
role = r
st.mu.Lock()
st.Plan.Squad.Agents = append(st.Plan.Squad.Agents, r)
st.mu.Unlock()
} else {
step.Status, step.Error = "failed", "role not found: "+step.Role
st.setStep(idx, step)
return d.failStep(ctx, st, idx)
}
}
subDir := filepath.Join(st.Meta.WorkDir, filepath.FromSlash(orDefault(step.SubWorkspace, fmt.Sprintf("tasks/%02d_%s", idx+1, step.Role))))
if err := os.MkdirAll(subDir, 0o755); err != nil {
return nil, nil, err
}
if step.SubWorkspace == "" {
step.SubWorkspace = filepath.ToSlash(filepath.Join("tasks", fmt.Sprintf("%02d_%s", idx+1, step.Role)))
}
d.emit(st, Event{Type: EvStepStarted, StepID: step.ID, Role: step.Role, Text: step.Title})
step.Status = "running"
step.Error = ""
st.setStep(idx, step)
started := time.Now()
prompt := readFileOr(filepath.Join(subDir, "TASK.md"), step.Title)
prompt = st.injectSteering(prompt)
instructions := readFileOr(filepath.Join(subDir, "AGENTS.md"), d.opts.Workspaces.ResolveInstructions(role))
backend, backendName, err := d.opts.Backends.Resolve(role, st.Meta.Backend)
if err != nil {
step.Status, step.Error = "failed", err.Error()
st.setStep(idx, step)
return d.failStep(ctx, st, idx)
}
targets := step.Targets
if len(targets) == 0 {
targets = []string{"output.md"}
}
var result *exec.Result
for attempt := 1; attempt <= exec.MaxTransientStepInvocations; attempt++ {
reqPrompt := prompt
if attempt > 1 {
prevReason := "交付物缺失"
if err != nil {
prevReason = err.Error()
} else if result != nil && result.ErrorReason != "" {
prevReason = result.ErrorReason
}
reqPrompt += "\n\n【重试提示】上一次执行未成功(" + prevReason + ")。请务必把结果写入:" + strings.Join(targets, ", ")
}
req := exec.Request{
WorkDir: subDir,
Prompt: reqPrompt,
Instructions: instructions,
Targets: targets,
Timeout: d.opts.StepTimeout,
ToolProfile: orDefault(role.ToolProfile, exec.ToolProfileWorkspace),
OnEvent: func(ev exec.ProcessEvent) {
d.emit(st, Event{Type: EvDetail, StepID: step.ID, Role: step.Role, Text: ev.Text,
Data: marshalJSON(map[string]string{"process_type": ev.Type, "tool": ev.Tool, "detail": ev.Detail})})
},
}
result, err = backend.Execute(ctx, req)
if err == nil && result.Success {
break
}
// 交付物物化:后端返回 patches 但未写盘时补写。
if err == nil && len(result.Patches) > 0 {
if _, merr := exec.MaterializeTargets(subDir, targets, result.Patches); merr == nil {
if allTargetsPresent(subDir, targets) {
result.Success = true
break
}
}
}
assessment := exec.AssessStepFailure(exec.StepFailureSignals{
Err: err, Result: result, WorkDir: subDir,
ExtraText: fmt.Sprintf("step=%s backend=%s", step.ID, backendName),
TargetMissing: !allTargetsPresent(subDir, targets),
})
if assessment.Class != exec.FailureTransient || attempt == exec.MaxTransientStepInvocations {
step.Status = "failed"
if err != nil {
step.Error = err.Error()
} else {
step.Error = orDefault(assessment.Reason, result.ErrorReason)
}
break
}
}
if ctx.Err() != nil {
step.Status = "pending" // 取消后可恢复
st.setStep(idx, step)
return &RunResult{RunID: st.Meta.ID, Status: RunCancelled, Error: "context cancelled",
Plan: planPtr(st), WorkDir: st.Meta.WorkDir}, nil, nil
}
if result != nil {
st.addUsage(step.Role, result.PromptTokens, result.CompletionTokens)
}
step.DurationMs = time.Since(started).Milliseconds()
if step.Status == "failed" {
d.emit(st, Event{Type: EvStepFailed, StepID: step.ID, Role: step.Role, Text: step.Error})
st.setStep(idx, step)
return d.failStep(ctx, st, idx)
}
// 交付物校验。
missing := missingTargets(subDir, targets)
if len(missing) > 0 {
step.Status, step.Error = "failed", fmt.Sprintf("missing deliverables: %v", missing)
d.emit(st, Event{Type: EvStepFailed, StepID: step.ID, Role: step.Role, Text: step.Error})
st.setStep(idx, step)
return d.failStep(ctx, st, idx)
}
step.Status = "completed"
appendLog(subDir, fmt.Sprintf("[%s] backend=%s duration=%dms targets=%v\n", time.Now().Format(time.RFC3339), backendName, step.DurationMs, targets))
// 状态回填 + 黑板合并 + 落盘同一临界区:并行步骤的共享状态变更串行化,
// 防止 marshal 与写回竞争、临时文件互相踩踏把保存失败误判为步骤失败。
st.mu.Lock()
st.Plan.Workflow.Steps[idx] = step
_ = mergeStepArtifacts(&st.BB, subDir)
err = st.save()
st.mu.Unlock()
if err != nil {
return nil, nil, err
}
d.emit(st, Event{Type: EvStepCompleted, StepID: step.ID, Role: step.Role, Text: step.Title,
Data: marshalJSON(map[string]any{"duration_ms": step.DurationMs, "targets": targets})})
return nil, nil, nil
}
// failStep 按关键性处理失败步骤:关键 → 运行失败;非关键 → 标记后继续(返回 nil 让循环推进)。
func (d *flowDriver) failStep(ctx context.Context, st *runState, idx int) (*RunResult, *hitlGate, error) {
st.mu.Lock()
step := st.Plan.Workflow.Steps[idx]
critical := step.IsCritical()
if critical {
wf := st.Workflow()
wf.Status = "failed"
st.setWorkflow(wf)
}
_ = st.save()
st.mu.Unlock()
if critical {
return &RunResult{
RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st), Error: fmt.Sprintf("step %s(%s) failed: %s", step.ID, step.Role, step.Error),
WorkDir: st.Meta.WorkDir,
}, nil, nil
}
// 非关键降级:标记 failed,工作流继续,收口时 degraded。
d.emit(st, Event{Type: EvStepSkipped, StepID: step.ID, Role: step.Role,
Text: "非关键步骤失败,降级继续: " + step.Error})
return nil, nil, nil
}
// checkBlockingGaps 检查未解决阻塞缺口;interactive 挂起,auto 记录兜底假设并继续。
func (d *flowDriver) checkBlockingGaps(st *runState) *hitlGate {
gaps := st.BB.UnresolvedBlockingGaps()
if len(gaps) == 0 {
return nil
}
mode := d.policy(st).mode()
if mode == HitlAuto {
for _, g := range gaps {
st.BB.Assumptions = append(st.BB.Assumptions, AssumptionItem{
ID: fmt.Sprintf("asm_%d", len(st.BB.Assumptions)+1), Topic: g.ID,
Statement: "无人值守模式:未获得人工输入,按现状继续推进(缺口保持未解决)",
Rationale: g.Item, Source: "fallback",
})
for i := range st.BB.Gaps {
if st.BB.Gaps[i].ID == g.ID {
st.BB.Gaps[i].Blocking = false
}
}
}
_ = st.save()
return nil
}
if mode == HitlDisabled {
return nil // 交由步骤级失败语义处理;缺口不再阻塞
}
var lines []string
for _, g := range gaps {
lines = append(lines, fmt.Sprintf("- [%s] %s(owner=%s)", g.ID, g.Item, g.Owner))
}
waitCond := waitConditionFrom(gaps)
req := &HumanRequest{
ID: newRequestID(), RunID: st.Meta.ID, Kind: HitlGap, Severity: SeverityActionRequired,
WaitCondition: waitCond,
Question: fmt.Sprintf("发现 %d 个阻塞缺口,需要人工补充信息后才能继续:", len(gaps)),
Options: []HumanOption{
{Value: "resolved", Label: "已在回答中补充信息,继续执行"},
{Value: "skip", Label: "跳过缺口,按现状继续"},
{Value: "abort", Label: "中止运行"},
},
Context: strings.Join(lines, "\n"),
CreatedAt: time.Now(), Status: "pending",
}
gate := &hitlGate{Request: *req, PrevStatus: RunExecuting}
if err := st.saveGate(gate); err == nil {
st.Meta.Status = string(RunWaitingHuman)
_ = st.save()
d.emitHuman(st, req)
}
return gate
}
// applyDynamicAdjustment 评估步骤产出并应用/审批动态调整。
func (d *flowDriver) applyDynamicAdjustment(ctx context.Context, st *runState, idx int) (*RunResult, *hitlGate, error) {
step := st.Plan.Workflow.Steps[idx]
subDir := filepath.Join(st.Meta.WorkDir, filepath.FromSlash(step.SubWorkspace))
output := readFileOr(filepath.Join(subDir, firstTarget(step.Targets)), "")
evalRes, err := d.opts.Evaluator.Evaluate(ctx, EvaluateRequest{
RunID: st.Meta.ID, Step: step, Workspace: st.Meta.WorkDir, Output: output, Blackboard: &st.BB,
})
if err != nil || evalRes == nil {
return nil, nil, err
}
for _, na := range evalRes.NewAssumptions {
st.BB.Assumptions = append(st.BB.Assumptions, na)
}
for _, gapID := range evalRes.ResolvedGaps {
st.BB.ResolveGapByID(gapID, "")
}
hasChanges := len(evalRes.DynamicStepsToAdd) > 0 || len(evalRes.StepsToSkip) > 0
if !hasChanges {
return nil, nil, nil
}
if evalRes.NeedsApproval && d.policy(st).mode() == HitlInteractive {
var addLines, skipLines []string
for _, s := range evalRes.DynamicStepsToAdd {
addLines = append(addLines, fmt.Sprintf("- %s(%s)%s", s.ID, s.Role, s.Title))
}
for _, id := range evalRes.StepsToSkip {
skipLines = append(skipLines, "- "+id)
}
req := &HumanRequest{
ID: newRequestID(), RunID: st.Meta.ID, StepID: step.ID, Kind: HitlAdjustment, Severity: SeverityAttention,
Question: "评估器建议动态调整工作流,是否应用?",
Options: []HumanOption{
{Value: "approve", Label: "应用调整"},
{Value: "reject", Label: "忽略调整"},
{Value: "abort", Label: "中止运行"},
},
Context: fmt.Sprintf("评估理由:%s\n追加步骤:\n%s\n跳过步骤:\n%s",
evalRes.Reason, strings.Join(addLines, "\n"), strings.Join(skipLines, "\n")),
CreatedAt: time.Now(), Status: "pending",
}
gate := &hitlGate{Request: *req, Steps: evalRes.DynamicStepsToAdd, SkipIDs: evalRes.StepsToSkip, PrevStatus: RunExecuting}
if err := st.saveGate(gate); err == nil {
st.Meta.Status = string(RunWaitingHuman)
_ = st.save()
d.emitHuman(st, req)
}
return heldResult(st, gate), gate, nil
}
applyAdjustment(st, evalRes.DynamicStepsToAdd, evalRes.StepsToSkip)
return nil, nil, nil
}
// applyAdjustment 应用动态调整:新步骤插到最后一步之前,跳过步骤标记 skipped。
func applyAdjustment(st *runState, add []StepSpec, skipIDs []string) {
steps := st.Plan.Workflow.Steps
for _, id := range skipIDs {
for i := range steps {
if steps[i].ID == id && steps[i].Status == "pending" {
steps[i].Status = "skipped"
}
}
}
if len(add) > 0 {
insertAt := len(steps)
if insertAt > 0 {
insertAt-- // 插在最后(成文/终审)步骤之前
}
for i := range add {
add[i].Status = "pending"
if add[i].Targets == nil {
add[i].Targets = []string{"notes.md", "fields.json"}
}
}
steps = append(steps[:insertAt], append(append([]StepSpec{}, add...), steps[insertAt:]...)...)
}
st.Plan.Workflow.Steps = steps
_ = st.save()
}
// finalize 收口:汇总交付物 → 质检 → 自动修复 → (必要时)人工裁决 → 清单。
func (d *flowDriver) finalize(ctx context.Context, st *runState) (*RunResult, *hitlGate, error) {
wf := st.Workflow()
degraded := false
for _, s := range wf.Steps {
if s.Status == "failed" || s.Status == "skipped" {
degraded = true
}
}
// 汇总交付物:最后一个 completed 步骤的主产出 → output.md。
var srcStep *StepSpec
for i := range wf.Steps {
if wf.Steps[i].Status == "completed" {
srcStep = &wf.Steps[i]
}
}
if srcStep == nil {
wf.Status = "failed"
st.setWorkflow(wf)
_ = st.save()
return &RunResult{RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st),
Error: "no completed step to assemble deliverable", WorkDir: st.Meta.WorkDir}, nil, nil
}
src := filepath.Join(st.Meta.WorkDir, filepath.FromSlash(srcStep.SubWorkspace), firstTarget(srcStep.Targets))
data, err := os.ReadFile(src)
if err != nil {
wf.Status = "failed"
st.setWorkflow(wf)
_ = st.save()
return &RunResult{RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st),
Error: "deliverable source missing: " + err.Error(), WorkDir: st.Meta.WorkDir}, nil, nil
}
deliverable := "output.md"
if err := os.WriteFile(filepath.Join(st.Meta.WorkDir, deliverable), data, 0o644); err != nil {
return nil, nil, err
}
d.emit(st, Event{Type: EvDeliverable, Text: deliverable})
// 质检 + 有界自动修复。
report := d.opts.Checker.Check(CheckRequest{
RunID: st.Meta.ID, Workspace: st.Meta.WorkDir, Deliverable: deliverable,
Plan: planPtr(st), Squad: &st.Plan.Squad, Template: st.Template,
})
if !report.Passed && st.RepairBudgetRemaining > 0 {
st.RepairBudgetRemaining--
_ = st.save()
// 重跑交付物源步骤一次,再检一次。
if out, g, err := d.rerunForRepair(ctx, st, srcStep.ID); out != nil || g != nil || err != nil {
return out, g, err
}
data2, _ := os.ReadFile(src)
_ = os.WriteFile(filepath.Join(st.Meta.WorkDir, deliverable), data2, 0o644)
report = d.opts.Checker.Check(CheckRequest{
RunID: st.Meta.ID, Workspace: st.Meta.WorkDir, Deliverable: deliverable,
Plan: planPtr(st), Squad: &st.Plan.Squad, Template: st.Template,
})
}
if !report.Passed {
switch d.policy(st).mode() {
case HitlDisabled, HitlAuto:
wf.Status = "failed"
st.setWorkflow(wf)
_ = st.save()
return &RunResult{RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st),
Quality: report, Error: "quality check failed (hitl mode=" + string(d.policy(st).mode()) + "): " + strings.Join(report.Violations, "; "),
WorkDir: st.Meta.WorkDir}, nil, nil
default:
req := &HumanRequest{
ID: newRequestID(), RunID: st.Meta.ID, Kind: HitlQualityReview, Severity: SeverityActionRequired,
Question: "交付物质检未通过且自动修复预算已用尽,请人工裁决:",
Options: []HumanOption{
{Value: "approve", Label: "人工放行(记录人工通过)"},
{Value: "retry", Label: "再修复一次(重跑交付步骤)"},
{Value: "abort", Label: "判定失败"},
},
Context: string(marshalJSON(report)),
CreatedAt: time.Now(), Status: "pending",
}
gate := &hitlGate{Request: *req, PrevStatus: RunExecuting}
if err := st.saveGate(gate); err == nil {
st.Meta.Status = string(RunQualityReview)
_ = st.save()
d.emitHuman(st, req)
}
return heldResult(st, gate), gate, nil
}
}
status := RunCompleted
if degraded {
status = RunDegraded
}
now := time.Now()
wf.Status = string(status)
st.setWorkflow(wf)
st.Meta.Status = string(status)
st.Meta.CompletedAt = &now
_ = st.save()
manifest := buildManifest(st, deliverable, *report)
_ = writeJSON(filepath.Join(st.Meta.WorkDir, "manifest.json"), manifest)
d.emit(st, Event{Type: EvRunCompleted, Text: string(status),
Data: marshalJSON(map[string]any{"deliverable": deliverable, "score": report.Score})})
return &RunResult{
RunID: st.Meta.ID, Status: status, Plan: planPtr(st), Deliverable: deliverable,
Quality: report, WorkDir: st.Meta.WorkDir, Usage: st.usageSnapshot(),
Summary: summarize(st, *report, status),
}, nil, nil
}
// rerunForRepair 重置交付物源步骤并重新执行到收口。
func (d *flowDriver) rerunForRepair(ctx context.Context, st *runState, stepID string) (*RunResult, *hitlGate, error) {
for i := range st.Plan.Workflow.Steps {
if st.Plan.Workflow.Steps[i].ID == stepID {
st.Plan.Workflow.Steps[i].Status = "pending"
st.Plan.Workflow.Steps[i].Error = ""
}
}
d.emit(st, Event{Type: EvPhase, Text: "质检未通过,自动修复:重跑交付步骤 " + stepID})
return d.advance(ctx, st)
}
// heldResult 由挂起上下文构造对外结果(挂起路径统一返回非 nil 结果)。
func heldResult(st *runState, gate *hitlGate) *RunResult {
status := RunWaitingHuman
switch gate.Request.Kind {
case HitlClarification:
status = RunAwaitingClarification
case HitlConfirmation:
status = RunAwaitingConfirmation
case HitlQualityReview:
status = RunQualityReview
}
return &RunResult{
RunID: st.Meta.ID, Status: status, Plan: planPtr(st),
Pending: &gate.Request, WorkDir: st.Meta.WorkDir, Usage: st.usageSnapshot(),
}
}
// waitConditionFrom 从阻塞缺口提取等待条件(首个声明者胜出)。
func waitConditionFrom(gaps []GapItem) *WaitCondition {
for _, g := range gaps {
if g.WaitRunID != "" {
return &WaitCondition{Type: "run_done", RunID: g.WaitRunID}
}
if g.WaitField != "" {
return &WaitCondition{Type: "field_set", Field: g.WaitField}
}
}
return nil
}
// hold 生成挂起请求并返回结果。
func (d *flowDriver) hold(st *runState, kind HumanRequestKind, stepID, role, question string, options []HumanOption, context string, extra *hitlGate) (*RunResult, *hitlGate, error) {
req := &HumanRequest{
ID: newRequestID(), RunID: st.Meta.ID, StepID: stepID, Kind: kind,
Severity: SeverityActionRequired,
Question: question, Options: options, Context: context,
CreatedAt: time.Now(), Status: "pending",
}
status := RunWaitingHuman
switch kind {
case HitlClarification:
status = RunAwaitingClarification
case HitlConfirmation:
status = RunAwaitingConfirmation
}
gate := extra
if gate == nil {
gate = &hitlGate{}
}
gate.Request = *req
gate.PrevStatus = RunExecuting
if err := st.saveGate(gate); err != nil {
return nil, nil, err
}
st.Meta.Status = string(status)
_ = st.save()
d.emitHuman(st, req)
return &RunResult{RunID: st.Meta.ID, Status: status, Plan: planPtr(st),
Pending: req, WorkDir: st.Meta.WorkDir}, nil, nil
}
// policy 合并引擎全局策略与 Goal 覆盖(窄者胜)。
func (d *flowDriver) policy(st *runState) HitlPolicy {
return mergeHitlPolicy(d.opts.HitlPolicy, st.Meta.HitlPolicyOf())
}
func (d *flowDriver) emit(st *runState, ev Event) {
if d.opts.Sink == nil {
return
}
ev.RunID = st.Meta.ID
if ev.At.IsZero() {
ev.At = time.Now()
}
d.opts.Sink.Emit(ev)
}
func (d *flowDriver) emitHuman(st *runState, req *HumanRequest) {
d.emit(st, Event{Type: EvHumanInputRequired, Text: req.Question, Data: marshalJSON(req)})
}
func summarize(st *runState, report QualityReport, status RunStatus) string {
var b strings.Builder
fmt.Fprintf(&b, "运行 %s:%s。步骤 %d 个(完成 %d/跳过 %d/失败 %d)。质检得分 %.0f。",
st.Meta.ID, status, len(st.Plan.Workflow.Steps),
countStatus(st, "completed"), countStatus(st, "skipped"), countStatus(st, "failed"), report.Score)
if gaps := st.BB.UnresolvedBlockingGaps(); len(gaps) > 0 {
fmt.Fprintf(&b, "未解决缺口 %d 个。", len(gaps))
}
return b.String()
}
func countStatus(st *runState, status string) int {
n := 0
for _, s := range st.Plan.Workflow.Steps {
if s.Status == status {
n++
}
}
return n
}
func buildManifest(st *runState, deliverable string, report QualityReport) Manifest {
files := map[string]string{}
_ = filepath.Walk(st.Meta.WorkDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
rel, _ := filepath.Rel(st.Meta.WorkDir, path)
files[filepath.ToSlash(rel)] = sha256File(path)
return nil
})
start := st.Meta.CreatedAt
return Manifest{
ManifestVersion: "1", WorkspaceID: st.Meta.ID, GeneratedAt: time.Now(),
Deliverable: deliverable, Files: files, QualityReport: report,
DurationMs: time.Since(start).Milliseconds(),
}
}
func firstTarget(targets []string) string {
if len(targets) > 0 {
return targets[0]
}
return "output.md"
}
func allTargetsPresent(dir string, targets []string) bool { return len(missingTargets(dir, targets)) == 0 }
func missingTargets(dir string, targets []string) []string {
var missing []string
for _, t := range targets {
if strings.TrimSpace(t) == "" {
continue
}
info, err := os.Stat(filepath.Join(dir, filepath.FromSlash(t)))
if err != nil || info.Size() == 0 {
missing = append(missing, t)
}
}
return missing
}
func readFileOr(path, fallback string) string {
if data, err := os.ReadFile(path); err == nil && len(strings.TrimSpace(string(data))) > 0 {
return string(data)
}
return fallback
}
func appendLog(dir, line string) {
f, err := os.OpenFile(filepath.Join(dir, "execution.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return
}
defer f.Close()
_, _ = f.WriteString(line)
}
func planPtr(st *runState) *WorkflowPlan {
wf := st.Workflow()
return &wf
}
func orDefault(v, def string) string {
if v == "" {
return def
}
return v
}