Files
Agent-System-Backend/internal/runtime/verify.go
T
weichen 5f9e77a500
ci / vet-and-test (pull_request) Successful in 40s
Consume the sunk gate mechanics and pin the WP5 library
Why: N1 WP5 host side - directRun.stop and the checklist reminders
now call the library gates package behind product ports, and the
nested native pin moves to the merged WP5 head.

Evidence: go vet clean; go test ./... 29 packages ok, including the
characterization suite unmodified and the new wp5_ab_test.go
(verify-cap continuation counts and gate-reason text parity).

Impact: llm.go drops the local regex family and deliverableGate;
verify.go drops checkDeliverables/parseVerdict/stripFence and the
local caps; workflow.go switches to gates.Verdict; reminders run
through gates.ProgressReminder. Behavior is pinned identical by the
existing suite plus the A/B tests. Squad reminders stay out of this
increment (adapter lacks per-member tool-turn signals - recorded as
WP6b).

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md WP5/WP6
2026-10-07 06:31:58 +00:00

176 lines
5.4 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 (
"fmt"
"strings"
"time"
"agent-native/runtime/gates"
agentllm "agent-native/runtime/llm"
core "agent-system-core"
)
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>"
}
func (l *LLMRunner) runVerifier(rc RunCtx, body string, evidence []toolEvidence, refs []*core.ArtifactRef) (gates.Verdict, error) {
route := ResolveLightweightModel(rc.Profile(), rc.Settings())
stream := l.VerifyStream
if stream == nil {
if route.Model == "" || route.Base == "" {
return gates.Verdict{}, 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 gates.Verdict{}, fmt.Errorf("验证器调用失败")
case agentllm.KindMessageEnd:
if b.Len() == 0 {
b.WriteString(ev.Message.Content)
}
}
}
verdict, err := gates.ParseVerdict(b.String())
if err != nil {
return gates.Verdict{}, 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 (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 < gates.ReminderToolGap {
return nil
}
// N1 WP5:节流、封顶与文案走下沉的 ProgressReminder;
// 工具轮计数留在这里(它与 directRun 的事件流耦合)。
if st.progress == nil {
st.progress = &gates.ProgressReminder{}
}
todos, plans := st.unfinishedItems()
got := st.progress.Remind(todos, plans, time.Now())
if got != nil {
st.toolTurnsSince = 0
}
return got
}
// unfinishedItems 返回计划与待办的未完成项正文(状态过滤留在产品侧)。
func (st *directRun) unfinishedItems() (todos, plans []string) {
for _, item := range st.todos.Items {
if item.Status == "done" {
continue
}
if text := strings.TrimSpace(item.Text); text != "" {
todos = append(todos, text)
}
}
for _, step := range st.plan.Steps {
if step.Status == "done" || step.Status == "skipped" {
continue
}
if text := strings.TrimSpace(step.Text); text != "" {
plans = append(plans, text)
}
}
return todos, plans
}