Why: N1 WP5/WP6 library side - the gate decision logic, artifact checks, verdict parsing and progress-reminder throttling move from the product into a gates package behind ports, and the native backend gains a contract form so conformance has a real implementation to drive. Evidence: go vet clean; go test ./... 27 packages ok, including the conformance suite (succeeded/descriptor/error-sideeffects) against the real backend. Impact: new runtime/gates package (ClaimGate, CheckArtifacts, ParseVerdict, ProgressReminder, shared caps); adapters/native.Backend gains Contract() returning an exec.ContractBackend wrapper - the legacy facade keeps its signature so squad consumption is untouched. Product texts (fix prompts, verifier prompts) stay injected by hosts. Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md WP5/WP6
79 lines
2.1 KiB
Go
79 lines
2.1 KiB
Go
package gates
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"agent-native/runtime/reminders"
|
|
)
|
|
|
|
// ProgressReminder 是进度提醒的节流状态机与格式化机制。
|
|
// 计数(第几个工具轮)由调用方按自己的事件流维护;本机制负责
|
|
// 「达到阈值后的去重、封顶与文案」。
|
|
type ProgressReminder struct {
|
|
Gap int // 连续多少个工具轮未更新即提醒;0 = ReminderToolGap
|
|
Cap int // 每 run 提醒上限;0 = MaxChecklistReminders
|
|
notes *reminders.Bus
|
|
sent int
|
|
}
|
|
|
|
func (r *ProgressReminder) gap() int {
|
|
if r.Gap <= 0 {
|
|
return ReminderToolGap
|
|
}
|
|
return r.Gap
|
|
}
|
|
|
|
func (r *ProgressReminder) cap() int {
|
|
if r.Cap <= 0 {
|
|
return MaxChecklistReminders
|
|
}
|
|
return r.Cap
|
|
}
|
|
|
|
// Sent 报告已发出的提醒条数(调用方判定封顶用)。
|
|
func (r *ProgressReminder) Sent() int { return r.sent }
|
|
|
|
// Remind 在给定未完成项时发一条提醒;节流或没有内容时返回 nil。
|
|
// 文案与产品现网一致:「计划和待办已连续 N 个工具轮没有更新」。
|
|
func (r *ProgressReminder) Remind(todos, plans []string, now time.Time) []string {
|
|
if r.sent >= r.cap() {
|
|
return nil
|
|
}
|
|
text := FormatUnfinished(r.gap(), todos, plans)
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
if r.notes == nil {
|
|
r.notes = reminders.New(0, r.cap())
|
|
}
|
|
key := fmt.Sprintf("%d:%s", r.sent, text)
|
|
if !r.notes.Admit(reminders.Note{Kind: reminders.KindTodo, Key: key, Text: text}, now) {
|
|
return nil
|
|
}
|
|
r.sent++
|
|
return []string{text}
|
|
}
|
|
|
|
// FormatUnfinished 渲染未完成项提醒;无内容时返回空串。
|
|
func FormatUnfinished(gap int, todos, plans []string) string {
|
|
var lines []string
|
|
for _, item := range todos {
|
|
if text := strings.TrimSpace(item); text != "" {
|
|
lines = append(lines, "- 待办:"+text)
|
|
}
|
|
}
|
|
for _, step := range plans {
|
|
if text := strings.TrimSpace(step); text != "" {
|
|
lines = append(lines, "- 计划:"+text)
|
|
}
|
|
}
|
|
if len(lines) == 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("计划和待办已连续 %d 个工具轮没有更新。未完成项:\n", gap) +
|
|
strings.Join(lines, "\n") +
|
|
"\n请更新计划或待办,或在回复里说明为什么先不做。"
|
|
}
|