feat(hitl): 增量2b —— 审批节点改 compose.Interrupt + orchestrator 识别中断
接了 checkpoint 后端时,审批节点从「阻塞 goroutine 等 5min」改为持久化中断:
首次执行发待审 + 置 waiting + compose.Interrupt → compose 把整图状态(含 board)
落进 checkpoint store 并返回中断错误 → Handle 释放 goroutine、任务停在 waiting,
不收尾不评测不判 done。抗 dispatcher 重启。
- orchestrator: 新增 errInterrupted 哨兵 + checkpoints 字段 + SetCheckpoints
setter(沿用 guard/usageSink 的 setter 注入,不动构造签名);Handle 识别
errInterrupted → 释放 goroutine、保留 waiting、SSE 流不关。
- compose_compiler: 编译挂 WithCheckPointStore + WithGraphName("root"),Invoke
带 WithCheckPointID(task_id);审批节点接 checkpoint 时改走专用
approvalInterruptLambda(须把中断错误作节点返回值上抛,泛型 lambda 会吞掉);
ExtractInterruptInfo 识别中断 → 上抛 errInterrupted。
- graph.go: 抽出 approvalSummary / applyApprovalDecision,阻塞式与中断式审批共用,
杜绝两路文案/语义漂移。
双路径并存:未接 checkpoint 后端(store=nil)维持阻塞模型,行为不变;main 暂不
接,生产保持阻塞,待增量3 resume 闭环补齐再打开(建在 flag 后)。
测试:中断半边端到端——errInterrupted + 置 waiting + checkpoint 落 KV(key=task_id)
+ 下游 agent 不执行。既有阻塞式审批/等价测试全绿。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -425,12 +425,7 @@ func (o *Orchestrator) branchNode(n dsl.Node, b *board, outs []dsl.Edge, byID ma
|
||||
// + 状态 waiting),阻塞等人工批准/拒绝(带超时,安全默认拒绝)。
|
||||
// 批准 → 状态回 running 并放行下游;拒绝/超时 → 置 b.rejected 中止全图。返回应激活的下游(拒绝=空)。
|
||||
func (o *Orchestrator) approvalNode(ctx context.Context, taskID string, n dsl.Node, b *board, tr *execTracer, outs []dsl.Edge) []string {
|
||||
title := firstNonEmpty(cstr(n.Config, "title"), labelOf(n, "人工审批"))
|
||||
prompt := firstNonEmpty(cstr(n.Config, "prompt"), "请审批是否继续执行后续步骤")
|
||||
summary := prompt
|
||||
if b.answer != "" { // 带上当前产出预览,便于审批人判断
|
||||
summary = prompt + "\n—— 当前产出预览 ——\n" + truncate(b.answer, 400)
|
||||
}
|
||||
title, summary := approvalSummary(n, b)
|
||||
|
||||
// 未接审批通道(单测/降级)→ 自动放行,避免无人应答卡死。
|
||||
if o.approval == nil {
|
||||
@@ -443,23 +438,51 @@ func (o *Orchestrator) approvalNode(ctx context.Context, taskID string, n dsl.No
|
||||
o.setStatus(taskID, contract.TaskWaiting, title)
|
||||
|
||||
dec, err := o.approval.WaitApproval(ctx, taskID, approvalTimeout)
|
||||
switch {
|
||||
case err != nil: // 超时 / ctx 取消 → 安全默认拒绝
|
||||
if err != nil { // 超时 / ctx 取消 → 安全默认拒绝
|
||||
b.rejected = true
|
||||
b.answer = "❌ 审批超时未决,已自动拒绝:" + title
|
||||
tr.emit("approval:"+n.ID, "approval", "error", title, "审批超时,自动拒绝", 0)
|
||||
return nil
|
||||
case !dec.Approved:
|
||||
}
|
||||
// 批准/拒绝的落盘与终态事件与中断式审批共用,杜绝两路文案/语义漂移。
|
||||
o.applyApprovalDecision(taskID, n, b, dec, dec.Approved, tr)
|
||||
if b.rejected {
|
||||
return nil
|
||||
}
|
||||
return targetsOf(outs)
|
||||
}
|
||||
|
||||
// approvalSummary 据审批节点配置 + 当前成稿,算出待审标题与摘要(带产出预览,便于审批人判断)。
|
||||
func approvalSummary(n dsl.Node, b *board) (title, summary string) {
|
||||
title = firstNonEmpty(cstr(n.Config, "title"), labelOf(n, "人工审批"))
|
||||
prompt := firstNonEmpty(cstr(n.Config, "prompt"), "请审批是否继续执行后续步骤")
|
||||
summary = prompt
|
||||
if b.answer != "" {
|
||||
summary = prompt + "\n—— 当前产出预览 ——\n" + truncate(b.answer, 400)
|
||||
}
|
||||
return title, summary
|
||||
}
|
||||
|
||||
// applyApprovalDecision 把一次人工决定落到黑板并发终态事件:拒绝 → 置 rejected + 拒绝语;
|
||||
// 批准 → 状态回 running 放行。阻塞式(approvalNode)与中断式(approvalInterruptLambda)审批共用。
|
||||
func (o *Orchestrator) applyApprovalDecision(taskID string, n dsl.Node, b *board, dec *contract.ApprovalDecision, approved bool, tr *execTracer) {
|
||||
title := firstNonEmpty(cstr(n.Config, "title"), labelOf(n, "人工审批"))
|
||||
if !approved {
|
||||
note := "审批人拒绝"
|
||||
if dec != nil {
|
||||
note = firstNonEmpty(dec.Note, note)
|
||||
}
|
||||
b.rejected = true
|
||||
note := firstNonEmpty(dec.Note, "审批人拒绝")
|
||||
b.answer = "❌ 已被拒绝:" + note
|
||||
tr.emit("approval:"+n.ID, "approval", "end", title, "拒绝:"+note, 0)
|
||||
return nil
|
||||
default: // 批准 → 恢复执行,放行下游
|
||||
o.setStatus(taskID, contract.TaskRunning, "审批通过,继续执行")
|
||||
tr.emit("approval:"+n.ID, "approval", "end", title, "批准:"+firstNonEmpty(dec.Note, "放行"), 0)
|
||||
return targetsOf(outs)
|
||||
return
|
||||
}
|
||||
note := "放行"
|
||||
if dec != nil {
|
||||
note = firstNonEmpty(dec.Note, note)
|
||||
}
|
||||
o.setStatus(taskID, contract.TaskRunning, "审批通过,继续执行")
|
||||
tr.emit("approval:"+n.ID, "approval", "end", title, "批准:"+note, 0)
|
||||
}
|
||||
|
||||
// targetsOf 取一组边的目标节点 ID(保持顺序)。
|
||||
|
||||
Reference in New Issue
Block a user