Files
Agent-System-Backend/internal/genui/auto.go
T
weichenandCursor 6ff7fa9bd7
ci / vet-and-test (pull_request) Successful in 1m4s
Add Generative UI auto-present opt-in with budget and cooldown
Why: Known-boundary follow-up: owner opt-in auto page suggestions on
semantic task events without self-trigger from surface revisions.
Evidence: go test ./internal/genui/ ./internal/runmgr/ ./internal/settings/ ./internal/app/
Impact: Default-off settings key; material/plan/todo/document hooks nudge present.
Refs: docs/proposals/2026-10-12-generative-ui-auto-trigger.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 03:44:47 +00:00

96 lines
3.0 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 genui
import (
"sync"
"time"
)
// AutoBudgetPerHour 是每任务(conversation)滑动一小时内允许的自动建议次数。
const AutoBudgetPerHour = 3
// AutoCooldown 是同会话两次自动触发的最小间隔。
const AutoCooldown = 60 * time.Second
// SettingAutoPresent 是 owner 级 opt-in 设置键(默认 false;与 activation 分离)。
const SettingAutoPresent = "plugins.generative-ui.autoPresent"
// AutoReminder 是注入 system-reminder 的建议文案(只引导 present,禁止改任务/点动作)。
const AutoReminder = "任务语义事件已发生(阶段或材料/工件完成)。若 generative-ui 已启用,请调用一次 generative-ui.present 生成或修订建议页,按当前任务阶段组织信息。不要点击页面 actions,不要改写 plan/todo/任务完成状态;任务事实只能来自真实任务记录。"
// ActivationLookup 查询 owner 的 Surface activation(generative-ui)。
type ActivationLookup func(owner string) (enabled bool)
// SettingsLookup 查询 owner 的 autoPresent opt-in。
type SettingsLookup func(owner string) (enabled bool)
// Auditor 写审计事件(耗尽等);nil = 跳过。
type Auditor func(owner, kind, detail string)
// Auto 是自动触发闸门:opt-in ∧ activation ∧ 预算 ∧ 冷却。
// 触发源由 runmgr 显式调用;present/surface/action 路径不得调用。
type Auto struct {
activation ActivationLookup
settings SettingsLookup
audit Auditor
now func() time.Time
mu sync.Mutex
budget map[string][]time.Time // convID → 触发时刻
last map[string]time.Time // convID → 上次成功放行
}
// NewAuto 构造闸门(lookup 不得为 nil)。
func NewAuto(activation ActivationLookup, settings SettingsLookup, audit Auditor) *Auto {
return &Auto{
activation: activation,
settings: settings,
audit: audit,
now: func() time.Time { return time.Now().UTC() },
budget: map[string][]time.Time{},
last: map[string]time.Time{},
}
}
// Try 尝试放行一次自动建议。ok=false 时 skip 含原因(供测试);ok=true 时调用方应注入 AutoReminder。
func (a *Auto) Try(owner, convID, reason string) (ok bool, skip string) {
if a == nil || a.activation == nil || a.settings == nil {
return false, "unconfigured"
}
if owner == "" || convID == "" {
return false, "missing_ids"
}
if !a.settings(owner) {
return false, "opt_in_off"
}
if !a.activation(owner) {
return false, "activation_off"
}
now := a.now()
a.mu.Lock()
defer a.mu.Unlock()
if t, hit := a.last[convID]; hit && now.Sub(t) < AutoCooldown {
return false, "cooldown"
}
cutoff := now.Add(-time.Hour)
kept := a.budget[convID][:0]
for _, t := range a.budget[convID] {
if t.After(cutoff) {
kept = append(kept, t)
}
}
a.budget[convID] = kept
if len(kept) >= AutoBudgetPerHour {
if a.audit != nil {
a.audit(owner, "genui.auto_present_exhausted", "conversation="+convID+" reason="+reason)
}
return false, "budget"
}
a.budget[convID] = append(kept, now)
a.last[convID] = now
return true, ""
}