Files
Cyrene_v2/internal/services/backthink/trigger.go
T
AskaEth 94da6bcd6d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(backthink): 静默期自排 C 的判定侧(C1-1,切片三之一)
- 追加 TriggerSelfWake(不动既有编号)+ SelfWakeRequest(走既有 urgency 声明通道,不新造第二条)
- escalateImport 折算收成唯一 foldDeclared(事件轮与自排轮共用、只上抬取较高档)
- DecisionInput.Heartbeat bool → Kind TriggerKind;心跳仍免检、**自排不享心跳豁免**(要求声明 ≥ warning)
- push 与档位分离:push.go 零改动即自动正确(eventDriven 对 self_wake 为 false ⇒ 照旧过 PushGate)
- 4 个变异自检(折算空操作/删自排分支/常量传心跳/非事件一律豁免)
⚠️ 本片只是判定侧:WakeNow/worker/闸属 C1-2,工具面与提示词属 C1-3 ⇒ **不得写『C 已完成』**
⚠️ 遗留 G-A(建议登记进 design/12 C.4):TriggerKind 零值=TriggerHeartbeat ⇒ 漏填=最宽豁免(fail-open);
   本片已在字段注释与单测里显式化,但规格侧应补一句或引入 TriggerUnknown
2026-10-06 18:46:27 +08:00

246 lines
11 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 backthink
import (
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
"git.yeij.top/AskaEth/Cyrene_v2/internal/urgency"
)
// TriggerKind 归类唤醒一轮思考的原因,好让推送路径套用
// 正确的策略:心跳受门控管辖(60min≤1 / 冷却 / 按风格),
// 事件是主人确实想听到的真实信号、绕过频率门。
type TriggerKind int
const (
// TriggerHeartbeat 是周期性自主反思。它的主动推送受
// PushGate 限流,使主人绝不会被空洞的反思刷屏。
TriggerHeartbeat TriggerKind = iota
// TriggerEvent 是真实事件(IoT 设备变化、提醒、与主人相关的
// 系统事件)。它的推送绕过 PushGate——"该发就发"——只受
// 同一事件去重约束。
TriggerEvent
// TriggerSelfWake 是她自己排起的一轮思考(design/12 §N C)。它既不是外部
// 事件、也不是周期性心跳:推送既不绕 PushGate,也不享长期静默段的豁免——
// 要求只是她说得出理由(见 decide.go 的 activityHeld)。
//
// 它是**追加**的第三个值:既有两个值的编号与标签逐字不变(改编号会
// 静默改写已落库记录与在途请求的含义)。
TriggerSelfWake
)
// String 返回用于日志与记录的稳定标签。
func (k TriggerKind) String() string {
switch k {
case TriggerEvent:
return "event"
case TriggerSelfWake:
return "self_wake"
case TriggerHeartbeat:
return "heartbeat"
default:
return fmt.Sprintf("triggerkind(%d)", int(k))
}
}
// 本服务为事件驱动思考订阅的总线主题。它们是
// design/12 §1 定义的契约:一次 IoT 状态变化、一次提醒触发,
// 或一次泛化的系统事件。
const (
// SubjectReminderTrigger 是提醒触发所用的总线主题。
SubjectReminderTrigger = "reminder.trigger"
// SubjectSystemEvent 是泛化系统事件的总线主题。
SubjectSystemEvent = "system.event"
)
// SelfWakeSource 是她自我唤醒(自排一次思考)的稳定来源标签。它同时是
// Trigger.Source、Record.Trigger、面板标签与审计前缀的**唯一**真源——
// 各处都不许再写这个字面量(另写一份就会与落库值静默漂移)。
const SelfWakeSource = "self_wake"
// SelfWakeSummaryPrefix 是自排轮触发摘要的稳定前缀(`self_wake: <她写的理由>`)。
// context.go 靠它认出「这一轮是她自己排起的」并切到专属引导提示词,
// 与 StartupSummary 是同一族的稳定标识。它由 SelfWakeSource 派生,
// 因此标签改一处即两处同改。
const SelfWakeSummaryPrefix = SelfWakeSource + ": "
// SystemEvent 是调用方可发布到 SubjectSystemEvent 以传达
// 系统通知的可选类型化负载。Import 默认为 Normal。在
// design/12 §6 策略一之下,低价值的系统通知(加入/退出 ping、泛化的
// 平台噪声)被降级为仅登记一条记录,不唤醒
// 一整轮思考;只有高重要度(与主人相关)的系统事件才思考,
// 因此繁忙的公开平台无法触发昂贵反思的洪流。
//
// 它是「载荷自带重要度」这条通用通道的既有形式;生产者的紧急度若以
// urgency.Declarer 声明(IoT 的设备跃迁即如此),由 triggerForEvent 折算到
// 同一个 Trigger.Import 字段上,两条形式因此落到同一套判定。
type SystemEvent struct {
Summary string
Import Importance
}
// SelfWakeRequest 是她自己发起的一次唤醒请求(design/12 §N C.1)。它实现
// urgency.Declarer,因此她的声明与 IoT 设备跃迁走**同一条**折算通道
// (foldDeclared → escalateImport),判定侧不为它加任何分支。
type SelfWakeRequest struct {
// Reason 是她写给「下一轮自己」的理由。它是有界的:截断在工具面完成
// (design/12 §C.3),这里只承载。
Reason string
// Declared 是她声明的紧急度;零值 = LevelRoutine = 不上抬。
Declared urgency.Level
}
// Urgency 让 SelfWakeRequest 满足 urgency.Declarer(与 SystemEvent 的
// 载荷自带重要度并列,不新造第二条通道)。
func (r SelfWakeRequest) Urgency() urgency.Level { return r.Declared }
var _ urgency.Declarer = SelfWakeRequest{}
// Trigger 描述唤醒一轮思考的原因:来源子系统、
// 可读摘要与事件的估计重要度。
type Trigger struct {
Source string
Summary string
Import Importance
// Kind 记录本轮是周期性心跳还是真实事件,好让
// 推送路径能为真实事件绕过频率门(主人要求)。
Kind TriggerKind
}
// heartbeatTrigger 是周期性、低重要度的触发。低重要度意味着
// 该轮很少打扰用户,遵守 "默认不发送 chat 类型消息"。
func heartbeatTrigger() Trigger {
return Trigger{Source: "heartbeat", Summary: "periodic autonomous reflection", Import: ImportanceLow, Kind: TriggerHeartbeat}
}
// StartupSummary 是启动轮思考(裁决 07.4.4,design/12 §1)的触发摘要。它同时是
// context.go 里切换「拟人化启动消息」专属引导提示词的标识。
const StartupSummary = "process startup"
// startupTrigger 是进程启动后的第一轮思考(裁决 07.4.4):不等满一个心跳周期,
// 启动立即触发,并引导她以角色口吻产出一条拟人化启动问候(如「我回来啦」)。
// Kind 仍为 TriggerHeartbeat:启动轮的推送与心跳走完全相同的门与限频
// (PushGate / 打扰级联 / 活动门),进程反复重启也不会刷屏主人。
func startupTrigger() Trigger {
return Trigger{Source: "startup", Summary: StartupSummary, Import: ImportanceLow, Kind: TriggerHeartbeat}
}
// triggerForEvent 把总线事件映射为 Trigger:主题给出可读摘要与主题级默认重要度,
// 随后**通用通道**(foldDeclared)统一解出重要度 —— 任何事件载荷只要实现
// urgency.Declarer(IoT 的 DeviceUpdate 是第一个使用者),它声明的紧急度就被承认;
// 泛化系统事件沿用它既有的 SystemEvent.Import 口径。两条形式最终都落在同一个
// Trigger.Import 字段上,判定侧(decide.go)因此只认档位、不认生产者。
// 每个事件都打上 TriggerEvent,因此其主动推送绕过心跳的 PushGate。
func triggerForEvent(e bus.Event) Trigger { return foldDeclared(triggerForSubject(e), e.Payload) }
// foldDeclared 是「生产者声明的档位」叠到「主题级重要度」上的**唯一**折算点,
// 也是 escalateImport 的唯一调用入口;触发者是谁(总线事件 / 她自己)不影响形状。
// 自排请求经 selfWakeTrigger 走这里,因此两个声明点(工具参数与结论尾段)
// 落在同一个 Trigger.Import 上、且都只上抬。
func foldDeclared(tr Trigger, payload any) Trigger {
if d, ok := payload.(urgency.Declarer); ok {
tr.Import = escalateImport(tr.Import, d.Urgency())
}
return tr
}
// selfWakeTrigger 把一次自我唤醒请求折算成 Trigger(与事件同一条链)。
// 主题级默认是 ImportanceNormal(不是 Low):未声明档位时,安静时段与长期静默段
// 照常扣下;Low 会自造出一档「比例行还低」的新语义,没有必要。
func selfWakeTrigger(req SelfWakeRequest) Trigger {
return foldDeclared(Trigger{
Source: SelfWakeSource, Summary: selfWakeSummary(req.Reason),
Import: ImportanceNormal, Kind: TriggerSelfWake,
}, req)
}
// selfWakeSummary 用稳定前缀承载她写的理由(前缀见 SelfWakeSummaryPrefix)。
func selfWakeSummary(reason string) string { return SelfWakeSummaryPrefix + reason }
// triggerForSubject 给出各主题的可读摘要与主题级默认重要度(未声明紧急度的事件就
// 停在这一档)。它刻意不对任何生产者做专有判定:重要度已由通用通道统一解出。
func triggerForSubject(e bus.Event) Trigger {
switch e.Subject {
case iot.DeviceUpdateSubject:
if d, ok := e.Payload.(iot.DeviceUpdate); ok {
return Trigger{
Source: e.Subject,
Summary: fmt.Sprintf("device %s (%s) %s", d.Device.ID, d.Device.Type, d.Action),
Import: ImportanceNormal,
Kind: TriggerEvent,
}
}
return Trigger{Source: e.Subject, Summary: e.Subject, Import: ImportanceNormal, Kind: TriggerEvent}
case SubjectReminderTrigger:
return Trigger{Source: e.Subject, Summary: e.Subject, Import: ImportanceHigh, Kind: TriggerEvent}
case SubjectSystemEvent:
tr := Trigger{Source: e.Subject, Summary: e.Subject, Import: ImportanceNormal, Kind: TriggerEvent}
if sy, ok := e.Payload.(SystemEvent); ok {
if sy.Summary != "" {
tr.Summary = sy.Summary
}
tr.Import = sy.Import
}
return tr
default:
return Trigger{Source: e.Subject, Summary: e.Subject, Import: ImportanceNormal, Kind: TriggerEvent}
}
}
// importanceForUrgency 把生产者声明的紧急度折算为思考轮的重要度。它是通用通道与
// 判定侧之间**唯一**的换算点(design/12 §6 安全域豁免,裁决 1.8 / 07.4.2):零值
// Routine 折算 Normal —— 默认档永不获得穿透安静时段的资格;只有生产者显式声明
// critical 的安全域事件拿到 ImportanceCritical,从而穿透安静时段与工作 / 忙碌。
func importanceForUrgency(l urgency.Level) Importance {
switch l {
case urgency.LevelCritical:
return ImportanceCritical
case urgency.LevelWarning:
return ImportanceHigh
default:
return ImportanceNormal
}
}
// urgencyForImportance 是 importanceForUrgency 的**反向读法**:把一轮思考**实际生效**
// 的重要度还原成通用紧急度词汇,供存档携带精确档位(X2a;面板此前只能给出「下限」)。
//
// 它复用同一张既有对应关系(Critical↔critical、High↔warning),不引入任何新的门限或
// 折算规则:例行档对应 importanceForUrgency 的 Normal,而 ImportanceLow 只是「比例行
// 还安静」的心跳/启动轮 —— urgency 词汇里没有更低的档,故与 Normal 同归 routine
// (无损:routine 本就表示「安静时段照常扣下」这一类)。按档位高低比较而不是逐值
// 相等,是让将来插入更高档时它落在最高档、而不是静默掉回 routine。
//
// 取数点在 backthink.go 的 think:入参是该轮**实际生效**的重要度
// (escalatedImport 的结果,她自报的紧急度已折叠在内)。
func urgencyForImportance(i Importance) urgency.Level {
switch {
case i >= ImportanceCritical:
return urgency.LevelCritical
case i >= ImportanceHigh:
return urgency.LevelWarning
default:
return urgency.LevelRoutine
}
}
// escalateImport 把声明的紧急度叠到基础重要度上。声明只能**上抬**、绝不压低,因此
// 主题级默认(如提醒的 High)与安全域事件既得的档位不会被一句 routine 抹掉;
// 未声明(零值 LevelRoutine)时原样返回 base,没标紧急度的事件行为逐字不变。
//
// 两条来源共用它:① foldDeclared —— 生产者声明的档位,它是唯一折算点,服务总线事件
// (triggerForEvent)与她自排的一轮(selfWakeTrigger)两种触发;② backthink.go 的
// escalatedImport —— 她自己在主动思考轮结论里声明「我认为这条紧急」。因此「工具参数
// urgency」与「结论尾段 [紧急:…]」两个声明点落在同一个 Trigger.Import 上,
// 该轮生效值取两者的较高档。
func escalateImport(base Importance, declared urgency.Level) Importance {
if declared == urgency.LevelRoutine {
return base
}
if up := importanceForUrgency(declared); up > base {
return up
}
return base
}