Files
Agent-Squad/squad/handoff.go
T
weichen c88100967d
ci / vet-and-test (pull_request) Successful in 34s
ci / docker-checkout-hostgateway (pull_request) Successful in 2s
Add review driver, handoff primitive, and group-chat driver (Q2 WP3)
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
2026-10-08 01:11:51 +00:00

92 lines
3.3 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 (
"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)})
}