Files
weichenandCursor 46ce1b2c8e Let subagents run in the background and be reviewed
Why: A parent turn should be able to finish while children keep working, and a review should keep only findings that a second pass confirms. Users also need to override a profile's tools, model, and turn cap without adding dispatch tools.
Evidence: go test ./... passed. New cases cover overlay rejection, role mapping, queued notices, and dropping FAIL or unparsed findings.
Impact: Notices live only in memory, and each conversation allows three background children. Squad writer steps now use the writer profile.
Refs: feature/agent-harness-p3b
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 15:29:01 +00:00

188 lines
4.5 KiB
Go
Raw Permalink 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 runtime
import (
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
core "agent-system-core"
)
const reviewProfileID = "agent.sub.review"
const verifyProfileID = "agent.sub.verify"
type reviewFinding struct {
Location string `json:"location"`
Severity string `json:"severity"`
Issue string `json:"issue"`
Evidence string `json:"evidence"`
}
func (st *directRun) reviewFindings(args string) string {
var a struct {
Target string
Focus string
Reviewers int
}
if err := json.Unmarshal([]byte(args), &a); err != nil {
return "评审参数错误: " + err.Error()
}
sp, ok := st.rc.(interface {
SpawnAgent(agentID, name, task, hint string) (core.AgentResultPayload, error)
})
if !ok {
return "当前运行不能派发评审子代理"
}
return runReview(sp.SpawnAgent, a.Target, a.Focus, a.Reviewers)
}
func runReview(spawn func(agentID, name, task, hint string) (core.AgentResultPayload, error), target, focus string, reviewers int) string {
if reviewers < 1 {
reviewers = 1
}
if reviewers > 3 {
reviewers = 3
}
found := make([][]reviewFinding, reviewers)
parallel(reviewers, func(i int) {
task := fmt.Sprintf("评审以下材料,重点是%s。只输出 JSON 数组,元素含 location、severity、issue、evidence。\n\n%s", orDefault(focus, "正确性与遗漏"), target)
res, err := spawn(reviewProfileID, fmt.Sprintf("审查%d", i+1), task, focus)
if err != nil {
return
}
found[i] = parseFindings(res.Text + "\n" + res.Summary)
})
unique := dedupeFindings(found, 10)
kept := make([]reviewFinding, len(unique))
pass := make([]bool, len(unique))
parallel(len(unique), func(i int) {
f := unique[i]
body, _ := json.Marshal(f)
task := fmt.Sprintf("独立核实这条问题是否成立。材料如下:\n%s\n\n问题:%s\n只在结尾给出一行 VERDICT: PASS、FAIL 或 PARTIAL。", target, body)
res, err := spawn(verifyProfileID, "核实", task, f.Location)
if err != nil {
return
}
if reviewVerdict(res.Text+"\n"+res.Summary) == "PASS" {
kept[i] = f
pass[i] = true
}
})
var verified []reviewFinding
for i, ok := range pass {
if ok {
verified = append(verified, kept[i])
}
}
sort.SliceStable(verified, func(i, j int) bool {
return severityRank(verified[i].Severity) < severityRank(verified[j].Severity)
})
dropped := len(unique) - len(verified)
var b strings.Builder
if len(verified) == 0 {
b.WriteString("没有验证通过的问题。\n")
}
for _, f := range verified {
fmt.Fprintf(&b, "- [%s] %s:%s\n 证据:%s\n", orDefault(f.Severity, "medium"), orDefault(f.Location, "未定位"), f.Issue, f.Evidence)
}
fmt.Fprintf(&b, "已丢弃 %d 条未验证\n", dropped)
return b.String()
}
func parallel(n int, fn func(i int)) {
if n <= 0 {
return
}
sem := make(chan struct{}, 3)
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
fn(i)
}(i)
}
wg.Wait()
}
func parseFindings(text string) []reviewFinding {
start := strings.Index(text, "[")
end := strings.LastIndex(text, "]")
if start < 0 || end <= start {
return nil
}
var out []reviewFinding
if err := json.Unmarshal([]byte(text[start:end+1]), &out); err != nil {
return nil
}
var kept []reviewFinding
for _, f := range out {
if strings.TrimSpace(f.Issue) == "" {
continue
}
kept = append(kept, f)
}
return kept
}
func dedupeFindings(groups [][]reviewFinding, limit int) []reviewFinding {
seen := map[string]bool{}
var out []reviewFinding
for _, group := range groups {
for _, f := range group {
key := strings.TrimSpace(f.Location) + "\n" + strings.TrimSpace(f.Issue)
if seen[key] {
continue
}
seen[key] = true
out = append(out, f)
if len(out) >= limit {
return out
}
}
}
return out
}
func reviewVerdict(text string) string {
lines := strings.Split(text, "\n")
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line == "" || !strings.HasPrefix(strings.ToUpper(line), "VERDICT:") {
continue
}
v := strings.TrimSpace(line[len("VERDICT:"):])
v = strings.ToUpper(v)
switch {
case strings.HasPrefix(v, "PASS"):
return "PASS"
case strings.HasPrefix(v, "FAIL"):
return "FAIL"
case strings.HasPrefix(v, "PARTIAL"):
return "PARTIAL"
default:
return ""
}
}
return ""
}
func severityRank(s string) int {
switch strings.ToLower(strings.TrimSpace(s)) {
case "critical":
return 0
case "high":
return 1
case "medium":
return 2
case "low":
return 3
default:
return 4
}
}