Files
Agent-System-Backend/internal/runtime/verify.go
T
weichenandCursor 78ce78a7d3 Cap tool results by character and gate stale file writes
Why: Byte truncation split UTF-8, subagent results could return unbounded text, and a write could overwrite a file that changed after it was read.
Evidence: cutRunes keeps a two-character Chinese prefix; tool selection honors -name; checklist reminders go through the reminder bus.
Impact: Tool output limits count characters. A subagent result over 32000 characters is stored as an artifact and the parent sees the path. fs_write_file rejects an existing file that was not read in this run.
Refs: feature/context-engineering
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 16:21:02 +00:00

279 lines
8.1 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 runtime
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"
"time"
agentllm "agent-native/runtime/llm"
"agent-native/runtime/reminders"
"agent-system-backend/internal/quality"
core "agent-system-core"
)
const (
// maxVerifyContinuations 与内核 Stop 的 3 次上限对齐。
maxVerifyContinuations = 3
// reminderToolGap 连续这么多个工具轮没有更新计划或待办,就提醒一次。
reminderToolGap = 3
// maxChecklistReminders 每个 run 最多提醒这么多次。
maxChecklistReminders = 3
)
const verifyCapNotice = "本轮回复未通过终轮检查。"
const verifierUnavailableNotice = "本轮回复未通过独立验证(验证器不可用)"
const verifyFallbackPrompt = `你是独立验证器,不调用任何工具。根据用户要求、终轮回复、工具证据和工件清单判断这一轮是否已经完成。
同一对象的后一次检查覆盖前一次,以最后一次为准。内置质量检查已通过时,不要用更早的工具输出推翻它。
拿不准时 passed 必须为 false。只输出一个 JSON 对象,不要写其他文字:{"passed":false,"reason":"为什么没通过","nextAction":"主模型下一步该做什么"}。`
// systemCheckReason 把终轮检查写成系统提醒。内核会把它当成一条用户消息送回模型,
// 不用这层包裹的话,模型会以为是用户自己提出的意见。
func systemCheckReason(body string) string {
body = strings.TrimSpace(body)
return "<system-reminder>\n这是系统的终轮自动检查,不是用户发言。先用工具核实;如果检查有误就直接收尾,不要向用户提起这条检查。\n" + body + "\n</system-reminder>"
}
type verifyVerdict struct {
Passed bool `json:"passed"`
Reason string `json:"reason"`
NextAction string `json:"nextAction"`
}
// checkDeliverables 确认本轮登记的工件存在且非空。
// md、txt、docx 还要过 quality;其他类型只检查存在与非空。
func checkDeliverables(rc RunCtx, refs []*core.ArtifactRef) []string {
var reasons []string
for _, ref := range refs {
if ref == nil || ref.ArtifactID == "" {
reasons = append(reasons, "工件引用缺少 ID")
continue
}
art, content, ok := rc.Artifact(ref.ArtifactID)
name := ref.Name
if name == "" {
name = string(ref.ArtifactID)
}
if !ok {
reasons = append(reasons, name+" 不存在")
continue
}
if art.Name != "" {
name = art.Name
}
if len(content) == 0 {
reasons = append(reasons, name+" 内容为空")
continue
}
switch strings.ToLower(filepath.Ext(name)) {
case ".md", ".txt", ".docx":
report := quality.ScoreBytes(name, content)
if !report.Passed {
why := strings.Join(report.Violations, ";")
if why == "" {
why = "评分未通过"
}
reasons = append(reasons, name+":"+why)
}
}
}
return reasons
}
func (l *LLMRunner) runVerifier(rc RunCtx, body string, evidence []toolEvidence, refs []*core.ArtifactRef) (verifyVerdict, error) {
route := ResolveLightweightModel(rc.Profile(), rc.Settings())
stream := l.VerifyStream
if stream == nil {
if route.Model == "" || route.Base == "" {
return verifyVerdict{}, fmt.Errorf("验证器调用失败")
}
client := openLLMClient(route.Base, route.Key)
stream = client.Stream
}
model := route.Model
if model == "" {
model = "verify"
}
msgs := []agentllm.Message{
{Role: agentllm.RoleSystem, Content: l.verifyPrompt(rc.Profile().Owner)},
{Role: agentllm.RoleUser, Content: evidencePrompt(currentUserText(rc), body, evidence, refs)},
}
var b strings.Builder
for ev := range stream(rc.Context(), model, msgs, nil, agentllm.StreamOptions{}) {
switch ev.Kind {
case agentllm.KindTextDelta:
b.WriteString(ev.Text)
case agentllm.KindError:
return verifyVerdict{}, fmt.Errorf("验证器调用失败")
case agentllm.KindMessageEnd:
if b.Len() == 0 {
b.WriteString(ev.Message.Content)
}
}
}
verdict, err := parseVerdict(b.String())
if err != nil {
return verifyVerdict{}, fmt.Errorf("验证器返回无法解析")
}
return verdict, nil
}
func (l *LLMRunner) verifyPrompt(owner string) string {
if l.Prompts != nil {
if body, ok, err := l.Prompts.ModuleBody(owner, "aux.verify.final"); err == nil && ok && strings.TrimSpace(body) != "" {
return body
}
}
return verifyFallbackPrompt
}
func evidencePrompt(user, body string, evidence []toolEvidence, refs []*core.ArtifactRef) string {
var b strings.Builder
b.WriteString("用户要求:\n")
if strings.TrimSpace(user) == "" {
b.WriteString("(无)\n")
} else {
b.WriteString(user)
b.WriteString("\n")
}
b.WriteString("\n终轮回复:\n")
b.WriteString(body)
b.WriteString("\n\n本轮工具(按时间编号。同一对象的后一次检查覆盖前一次,以最后一次为准):\n")
if len(evidence) == 0 {
b.WriteString("(无)\n")
}
for i, ev := range evidence {
text := ev.VerifyText
if text == "" {
text = ev.Summary
}
fmt.Fprintf(&b, "#%d %s %s %s\n", i+1, ev.Name, ev.Status, text)
}
b.WriteString("\n本轮工件:\n")
if len(refs) == 0 {
b.WriteString("(无)\n")
}
for _, ref := range refs {
if ref == nil {
continue
}
fmt.Fprintf(&b, "- %s %s\n", ref.ArtifactID, ref.Name)
}
b.WriteString("\n内置质量检查:已通过。")
if len(refs) > 0 {
b.WriteString("工件存在且非空;md、txt、docx 的版式评分已通过。这是强证据,不要用更早的工具输出推翻它。")
}
b.WriteString("\n")
return b.String()
}
func parseVerdict(raw string) (verifyVerdict, error) {
s := stripFence(raw)
start := strings.Index(s, "{")
end := strings.LastIndex(s, "}")
if start < 0 || end < start {
return verifyVerdict{}, fmt.Errorf("验证器返回无法解析")
}
s = s[start : end+1]
var probe map[string]json.RawMessage
if err := json.Unmarshal([]byte(s), &probe); err != nil {
return verifyVerdict{}, err
}
if _, ok := probe["passed"]; !ok {
return verifyVerdict{}, fmt.Errorf("验证器返回无法解析")
}
var verdict verifyVerdict
if err := json.Unmarshal([]byte(s), &verdict); err != nil {
return verifyVerdict{}, err
}
return verdict, nil
}
func stripFence(s string) string {
s = strings.TrimSpace(s)
if !strings.HasPrefix(s, "```") {
return s
}
s = strings.TrimPrefix(s, "```")
if i := strings.IndexByte(s, '\n'); i >= 0 {
s = s[i+1:]
}
s = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(s), "```"))
return s
}
func (st *directRun) loadChecklist() {
if src, ok := st.rc.(interface{ Todos() (core.TodoList, bool) }); ok {
if list, ok := src.Todos(); ok {
st.todos = list
}
}
if src, ok := st.rc.(interface {
Plan() (core.PlanSnapshot, bool)
}); ok {
if p, ok := src.Plan(); ok {
st.plan = p
}
}
}
func (st *directRun) reminders(turn int) []string {
if st.closedToolTurn {
if st.checklistTouched {
st.toolTurnsSince = 0
} else {
st.toolTurnsSince++
}
st.closedToolTurn = false
st.checklistTouched = false
}
if st.toolTurnsSince < reminderToolGap || st.checklistReminders >= maxChecklistReminders {
return nil
}
text := st.unfinishedReminder()
if text == "" {
return nil
}
if st.notes == nil {
st.notes = reminders.New(0, maxChecklistReminders)
}
key := fmt.Sprintf("%d:%s", st.checklistReminders, text)
if !st.notes.Admit(reminders.Note{Kind: reminders.KindTodo, Key: key, Text: text}, time.Now()) {
return nil
}
st.toolTurnsSince = 0
st.checklistReminders++
return []string{text}
}
func (st *directRun) unfinishedReminder() string {
var lines []string
for _, item := range st.todos.Items {
if item.Status == "done" {
continue
}
text := strings.TrimSpace(item.Text)
if text == "" {
continue
}
lines = append(lines, "- 待办:"+text)
}
for _, step := range st.plan.Steps {
if step.Status == "done" || step.Status == "skipped" {
continue
}
text := strings.TrimSpace(step.Text)
if text == "" {
continue
}
lines = append(lines, "- 计划:"+text)
}
if len(lines) == 0 {
return ""
}
return "计划和待办已连续 3 个工具轮没有更新。未完成项:\n" + strings.Join(lines, "\n") + "\n请更新计划或待办,或在回复里说明为什么先不做。"
}