Why: Q2 acceptance 2 requires the squad review topology to be equivalent to today's backend runReview flow (parallel finders, per-finding verification, drop unverified); handoff and group_chat complete 11 of 12 builtin topologies. Evidence: wp3_conformance_test.go mirrors backend review_test.go assertions (true finder concurrency via release gate, verify calls = 5 unique after dedupe, only PASS kept, dropped-count line, severity order); handoff records ownership transfer, target receives filtered history, finalize takes chain-tail output, unknown target role fails closed; group-chat rotate / close-marker / moderator conformance with budget stop and fail-closed unknown nominee. Full suite green with -race. Impact: review/group_chat drivers lit in the topology registry (task_board remains fail-closed until WP4); RunState gains Handoffs (persisted); RunOptions gains GroupChat*/Review* knobs; runOneStep validates handoff targets and injects handoff context; finalize prefers chain-tail deliverable. Parse/dedupe/verdict/severity/report formats ported verbatim from backend internal/runtime/review.go for byte-level report equivalence. Refs: Q2 plan WP3 (2026-10-07-agent-stack-q2q3-plans.md); task card .task/feature-agent-stack-q2.md
92 lines
3.3 KiB
Go
92 lines
3.3 KiB
Go
package squad
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// handoff.go — handoff 跨拓扑原语(Q2 WP3)。
|
||
// 语义(计划 §三 WP3):StepSpec.Handoff 非空的步骤完成后,最终答案所有权
|
||
// 移交给目标角色——① 记录所有权转移(Handoffs,随 RunState 持久化);
|
||
// ② 目标角色的下一个步骤收到「过滤历史」:只带移交步骤的主产出,不带全量
|
||
// 转录(一次性并入任务书,与 mailbox 同款机制);③ finalize 汇总交付物时
|
||
// 取链尾(最后一次移交的目标角色)的最后一个完成步骤产出,而非全图最后
|
||
// 完成步骤。目标角色不在小队内 = fail-closed(步骤启动前拒绝)。
|
||
|
||
// HandoffRecord 是一次所有权转移记录。
|
||
type HandoffRecord struct {
|
||
FromRole string `json:"from_role"`
|
||
FromStep string `json:"from_step"`
|
||
ToRole string `json:"to_role"`
|
||
Output string `json:"output,omitempty"` // 移交步骤主产出(截断上限内)
|
||
At time.Time `json:"at"`
|
||
Delivered bool `json:"delivered,omitempty"`
|
||
}
|
||
|
||
// handoffOutputCap 移交上下文的产出截断上限(过滤历史:只带主产出,且限量)。
|
||
const handoffOutputCap = 64 * 1024
|
||
|
||
// takeHandoffFor 取出移交给某角色的未投递记录,标记已投递(一次性)。
|
||
// 调用方负责随后 st.save()(或在临界区内)。
|
||
func takeHandoffFor(st *RunState, role string) *HandoffRecord {
|
||
for i := range st.Handoffs {
|
||
h := &st.Handoffs[i]
|
||
if h.Delivered || h.ToRole != role {
|
||
continue
|
||
}
|
||
h.Delivered = true
|
||
out := *h
|
||
return &out
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// formatHandoffSection 渲染任务书里的移交上下文段(无则空串)。
|
||
func formatHandoffSection(h *HandoffRecord) string {
|
||
if h == nil {
|
||
return ""
|
||
}
|
||
body := h.Output
|
||
if len(body) > handoffOutputCap {
|
||
body = body[:handoffOutputCap] + "\n…(已截断)"
|
||
}
|
||
var b strings.Builder
|
||
fmt.Fprintf(&b, "\n\n## 移交上下文(所有权来自 %s,步骤 %s)\n", h.FromRole, h.FromStep)
|
||
b.WriteString("以下是移交方的主产出(过滤历史:只含该产出,不含其过程转录):\n\n")
|
||
b.WriteString(body)
|
||
b.WriteString("\n")
|
||
return b.String()
|
||
}
|
||
|
||
// handoffChainTail 返回移交链尾角色(无移交记录返回空串)。
|
||
func handoffChainTail(st *RunState) string {
|
||
if n := len(st.Handoffs); n > 0 {
|
||
return st.Handoffs[n-1].ToRole
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// recordHandoff 步骤完成时的所有权转移记录:主产出快照(截断上限内)+
|
||
// 持久化 + 事件。目标角色合法性已在步骤启动前 fail-closed 校验。
|
||
func (d *flowDriver) recordHandoff(st *RunState, step StepSpec, subDir string, targets []string) {
|
||
output := ""
|
||
if data, err := os.ReadFile(filepath.Join(subDir, firstTarget(targets))); err == nil {
|
||
output = string(data)
|
||
}
|
||
if len(output) > handoffOutputCap {
|
||
output = output[:handoffOutputCap] + "\n…(已截断)"
|
||
}
|
||
st.mu.Lock()
|
||
st.Handoffs = append(st.Handoffs, HandoffRecord{
|
||
FromRole: step.Role, FromStep: step.ID, ToRole: step.Handoff,
|
||
Output: output, At: time.Now(),
|
||
})
|
||
_ = st.save()
|
||
st.mu.Unlock()
|
||
d.emit(st, Event{Type: EvPhase, StepID: step.ID, Role: step.Role,
|
||
Text: fmt.Sprintf("handoff:%s → %s(最终答案所有权移交)", step.Role, step.Handoff)})
|
||
}
|