ci / vet-and-test (pull_request) Successful in 40s
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
176 lines
5.4 KiB
Go
176 lines
5.4 KiB
Go
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
|
||
}
|