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
This commit is contained in:
1 parent
ea43f07bce
commit
94da6bcd6d
8 files changed
+353
-26
No files matched your search
@@ -380,7 +380,7 @@ func (b *Backthink) think(parent context.Context, tr Trigger) {
|
||||
LastActivity: lastActivity,
|
||||
HasActivity: hasActivity,
|
||||
ConclusionPush: push,
|
||||
Heartbeat: tr.Kind == TriggerHeartbeat,
|
||||
Kind: tr.Kind,
|
||||
})
|
||||
shouldPush := decision.ShouldPush
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ func (i Importance) String() string {
|
||||
|
||||
// longIdleThreshold 是主人最后一条消息之后的间隔,超过它活动门
|
||||
// 只放行真正重要的事件——但心跳轮除外,
|
||||
// 它们永远不会被长期静默的间隔拦下(裁决 07.4.3)。编译期
|
||||
// 它们永远不会被长期静默的间隔拦下(裁决 07.4.3)。自排轮也不享这条豁免
|
||||
// (design/12 §N C.4):要求她声明 ≥ warning 才放行。编译期
|
||||
// 常量,因此不出现魔法数字。
|
||||
const longIdleThreshold = 2 * time.Hour
|
||||
|
||||
@@ -95,9 +96,15 @@ type DecisionInput struct {
|
||||
// ConclusionPush 是她自己思考结论的推送意愿(design/20 §2 P2):「想不想说」
|
||||
// 由她的结论决定,本决策只回答「此刻能不能说」。
|
||||
ConclusionPush bool
|
||||
// Heartbeat 标记本轮是否为心跳类触发(周期心跳或启动轮)。裁决 07.4.3:
|
||||
// 长期静默段不拦心跳——主人久未回应时,主动关心仍可照常发出。
|
||||
Heartbeat bool
|
||||
// Kind 是本轮的触发类别(周期心跳 / 启动轮 / 真实事件 / 自我唤醒),取代了
|
||||
// 原来的布尔 Heartbeat —— 布尔表达不了三种触发(design/12 §N C.4)。判定侧只
|
||||
// 需要区分「周期性心跳(含启动轮,享长期静默豁免)」与「她自己排起的一轮
|
||||
// (要求她说得出理由)」,其余类别按事件处理。
|
||||
//
|
||||
// ⚠️ TriggerKind 的零值是 TriggerHeartbeat:生产调用方**必须**显式传 tr.Kind
|
||||
// (backthink.go 的 think 是唯一调用点,已显式传)。省略 Kind 等于按心跳豁免,
|
||||
// 那是最宽的一档,别让「漏填」静默变成「免检」。
|
||||
Kind TriggerKind
|
||||
}
|
||||
|
||||
// Decision 是 Decide 的结果。
|
||||
@@ -120,7 +127,8 @@ type Decision struct {
|
||||
// 4. 其余时段 → 放行(是否值得说已由她的结论回答)。
|
||||
//
|
||||
// 通过硬门后再过活动软门:刚聊过天或主人久未回应时酌情扣下;心跳类触发在
|
||||
// 长期静默段不拦(裁决 07.4.3,主动关心照常发出)。频率门(PushGate)在推送器
|
||||
// 长期静默段不拦(裁决 07.4.3,主动关心照常发出),自排轮则要求她说得出理由
|
||||
// (design/12 §N C.4)。频率门(PushGate)在推送器
|
||||
// 内部单独生效,不在此处。
|
||||
func (b *Backthink) Decide(in DecisionInput) Decision {
|
||||
if !in.ConclusionPush {
|
||||
@@ -166,7 +174,8 @@ func (b *Backthink) baseDecide(in DecisionInput) Decision {
|
||||
// 不在聊天中途插话;
|
||||
// - 超过 longIdleThreshold 未说话 → 主人久未回应,仅真正重要的事件值得唤醒;
|
||||
// 心跳类触发(含启动轮)例外照常放行(裁决 07.4.3:长期静默段不拦心跳,
|
||||
// 主动关心仍可发出);
|
||||
// 主动关心仍可发出);她自己排起的一轮**不享**这条豁免(design/12 §N C.4)
|
||||
// ——要求她说得出理由(Import ≥ High,即声明 ≥ warning),否则扣下;
|
||||
// - 其间与活动状态未知 → 不软化,照硬门结论通过。
|
||||
//
|
||||
// 活动是软信号:硬门(baseDecide)独立生效,频率门(PushGate)在推送器内部生效。
|
||||
@@ -182,12 +191,15 @@ func (b *Backthink) activityHeld(in DecisionInput) (bool, string) {
|
||||
return true, "owner just chatted"
|
||||
}
|
||||
if since >= longIdleThreshold {
|
||||
if in.Heartbeat {
|
||||
return false, ""
|
||||
if in.Kind == TriggerHeartbeat {
|
||||
return false, "" // 07.4.3 原样:例行心跳(含启动轮)不拦
|
||||
}
|
||||
if in.Import >= ImportanceHigh {
|
||||
return false, ""
|
||||
}
|
||||
if in.Kind == TriggerSelfWake {
|
||||
return true, "self-initiated wake held: owner away and no declared urgency"
|
||||
}
|
||||
return true, "owner away a long while"
|
||||
}
|
||||
return false, ""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package backthink
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -59,7 +60,7 @@ func TestDecide_DoNotDisturb(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "安静时段_心跳_不打扰",
|
||||
in: DecisionInput{Trigger: "heartbeat", Import: ImportanceLow, At: nightAt(), UserState: UserState{}, ConclusionPush: true, Heartbeat: true},
|
||||
in: DecisionInput{Trigger: "heartbeat", Import: ImportanceLow, At: nightAt(), UserState: UserState{}, ConclusionPush: true, Kind: TriggerHeartbeat},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
@@ -171,7 +172,8 @@ func TestClassifyPush(t *testing.T) {
|
||||
// TestDecide_ActivityGate 验证活动感知软门:刚开始的
|
||||
// 对话会扣下琐碎内容(critical 除外),长期静默的主人只会被
|
||||
// 重要事件唤醒——但心跳轮除外,它永远不会被长期
|
||||
// 静默的间隔拦下(裁决 07.4.3)——而中间地带不会过度抑制。
|
||||
// 静默的间隔拦下(裁决 07.4.3),而她自己排起的一轮**不享**这条豁免
|
||||
// (design/12 §N C.4,见紧随其后的用例)——中间地带不会过度抑制。
|
||||
func TestDecide_ActivityGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := testBackthink()
|
||||
@@ -185,6 +187,7 @@ func TestDecide_ActivityGate(t *testing.T) {
|
||||
LastActivity: dayAt().Add(-5 * time.Minute),
|
||||
HasActivity: true,
|
||||
ConclusionPush: true,
|
||||
Kind: TriggerEvent,
|
||||
}
|
||||
if got := b.Decide(fresh); got.ShouldPush {
|
||||
t.Fatalf("fresh conversation should hold a normal push, got %+v", got)
|
||||
@@ -196,6 +199,8 @@ func TestDecide_ActivityGate(t *testing.T) {
|
||||
|
||||
// 离开很久(> longIdleThreshold):只有重要事件值得
|
||||
// 唤醒——但心跳轮仍会通过(裁决 07.4.3 长期静默段不拦心跳)。
|
||||
// Kind 必须显式给出:TriggerKind 的零值是 TriggerHeartbeat,
|
||||
// 省掉它这一档就从「事件」静默变成「心跳豁免」。
|
||||
idle := DecisionInput{
|
||||
Import: ImportanceNormal,
|
||||
At: dayAt(),
|
||||
@@ -203,6 +208,7 @@ func TestDecide_ActivityGate(t *testing.T) {
|
||||
LastActivity: dayAt().Add(-3 * time.Hour),
|
||||
HasActivity: true,
|
||||
ConclusionPush: true,
|
||||
Kind: TriggerEvent,
|
||||
}
|
||||
if got := b.Decide(idle); got.ShouldPush {
|
||||
t.Fatalf("long idle should hold a normal event push, got %+v", got)
|
||||
@@ -212,7 +218,7 @@ func TestDecide_ActivityGate(t *testing.T) {
|
||||
t.Fatalf("long idle high-importance should push, got %+v", got)
|
||||
}
|
||||
idle.Import = ImportanceLow
|
||||
idle.Heartbeat = true
|
||||
idle.Kind = TriggerHeartbeat
|
||||
if got := b.Decide(idle); !got.ShouldPush {
|
||||
t.Fatalf("long idle heartbeat should still push (07.4.3), got %+v", got)
|
||||
}
|
||||
@@ -225,8 +231,58 @@ func TestDecide_ActivityGate(t *testing.T) {
|
||||
LastActivity: dayAt().Add(-30 * time.Minute),
|
||||
HasActivity: true,
|
||||
ConclusionPush: true,
|
||||
Kind: TriggerEvent,
|
||||
}
|
||||
if got := b.Decide(middle); !got.ShouldPush {
|
||||
t.Fatalf("middle-band activity should allow a normal push, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecide_ActivityGate_SelfWakeNeedsDeclaredUrgency 是 design/12 §C.4 的真断言
|
||||
// (判据 4):长期静默段里,她自己排起的一轮要求她说得出理由(折算后 Import ≥ High,
|
||||
// 即声明 ≥ warning)才放行;例常自排被扣下、理由点名「self-initiated wake」;
|
||||
// 而例行心跳的既有豁免**不许回归**(裁决 07.4.3)。把 activityHeld 里的自排分支
|
||||
// 删掉、或把三分支改回「非事件一律豁免」,这条即变红。
|
||||
func TestDecide_ActivityGate_SelfWakeNeedsDeclaredUrgency(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := testBackthink()
|
||||
in := DecisionInput{
|
||||
Trigger: SelfWakeSource,
|
||||
Import: ImportanceNormal,
|
||||
At: dayAt(),
|
||||
UserState: UserState{Boredom: 0.9},
|
||||
LastActivity: dayAt().Add(-3 * time.Hour),
|
||||
HasActivity: true,
|
||||
ConclusionPush: true,
|
||||
Kind: TriggerSelfWake,
|
||||
}
|
||||
|
||||
got := b.Decide(in)
|
||||
if got.ShouldPush {
|
||||
t.Fatalf("undeclared self wake in long silence should be held, got %+v", got)
|
||||
}
|
||||
if !strings.Contains(got.Reason, "self-initiated wake") {
|
||||
t.Fatalf("Reason = %q, want it to name the self-initiated wake hold", got.Reason)
|
||||
}
|
||||
|
||||
// 她声明了 warning ⇒ selfWakeTrigger 折算成 High ⇒ 放行。
|
||||
in.Import = ImportanceHigh
|
||||
if got := b.Decide(in); !got.ShouldPush {
|
||||
t.Fatalf("self wake with a declared reason (>= warning) should push, got %+v", got)
|
||||
}
|
||||
|
||||
// 对照:同样久未回应,例行心跳照旧放行(07.4.3 豁免不许被这次改动带走)。
|
||||
hb := DecisionInput{
|
||||
Trigger: "heartbeat",
|
||||
Import: ImportanceLow,
|
||||
At: dayAt(),
|
||||
UserState: UserState{Boredom: 0.9},
|
||||
LastActivity: dayAt().Add(-3 * time.Hour),
|
||||
HasActivity: true,
|
||||
ConclusionPush: true,
|
||||
Kind: TriggerHeartbeat,
|
||||
}
|
||||
if got := b.Decide(hb); !got.ShouldPush {
|
||||
t.Fatalf("long idle heartbeat should still push (07.4.3), got %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,17 @@ package backthink
|
||||
// 门(主人要求:"该发就发"——真正的事件照发,只扣下真正的重复)。
|
||||
//
|
||||
// 推送进行时它原子地把 tr 记为最新事件键,并在任何非事件推送
|
||||
// (心跳)时清除该键,以免更早事件的再次重复
|
||||
// 被中间一次反思错误地抑制。
|
||||
// (非事件轮:心跳 / 启动轮 / 她自己排起的一轮)时清除该键,以免更早事件的再次
|
||||
// 重复被中间一次反思错误地抑制。
|
||||
//
|
||||
// 自排轮与心跳同分支(清链、不去重):它既不是外部事件、就不该参与事件链,
|
||||
// 因此真实事件的重复链不会被自排轮错误抑制;自排轮的推送仍由 PushGate 兜。
|
||||
//
|
||||
// 调用方把它跑在把推送交给 Pusher 之前;真正的重复
|
||||
// 会被记为已抑制(RouteNone),而不是投递出去。
|
||||
func (b *Backthink) eventRedundant(tr Trigger) bool {
|
||||
if tr.Kind != TriggerEvent {
|
||||
// 非事件推送(心跳)会打断同一事件链。
|
||||
// 非事件轮(心跳 / 启动轮 / 自我唤醒轮)会打断同一事件链。
|
||||
b.eventMu.Lock()
|
||||
b.lastEventKey = ""
|
||||
b.eventMu.Unlock()
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package backthink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/urgency"
|
||||
)
|
||||
|
||||
// 本文件守住 design/12 §N C 的**判定侧**(切片 C1-1):自我唤醒是追加的第三个
|
||||
// TriggerKind,它的档位折算与总线事件共用同一条通道(foldDeclared → escalateImport),
|
||||
// 判定侧只多认一个 kind、不加第二条档位通道。对应 C.8 判据 1 / 4 / 6 / 9。
|
||||
//
|
||||
// ⚠️ 本片**不产生**自排轮:WakeNow / worker / 工具面分别属 C1-2 / C1-3,因此这里的
|
||||
// 「生产路径」用例直接构造 selfWakeTrigger —— 它证明的是链路已就绪,不是能力已交付。
|
||||
|
||||
// TestTriggerKind_SelfWakeAppendedWithStableLabels 钉住标签与编号口径(C.6 落地清单):
|
||||
// 落库值(Record.Trigger)、面板值与审计前缀共用同一个真源;新 kind 是**追加**的,
|
||||
// 既有两个值的编号与标签逐字不变(改编号会静默改写已落库记录的含义)。
|
||||
func TestTriggerKind_SelfWakeAppendedWithStableLabels(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
kind TriggerKind
|
||||
want string
|
||||
}{
|
||||
{TriggerHeartbeat, "heartbeat"},
|
||||
{TriggerEvent, "event"},
|
||||
{TriggerSelfWake, SelfWakeSource},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := tc.kind.String(); got != tc.want {
|
||||
t.Fatalf("TriggerKind(%d).String() = %q, want %q", int(tc.kind), got, tc.want)
|
||||
}
|
||||
}
|
||||
if !(TriggerHeartbeat < TriggerEvent && TriggerEvent < TriggerSelfWake) {
|
||||
t.Fatalf("kind 编号不再是追加的:heartbeat=%d event=%d self_wake=%d",
|
||||
int(TriggerHeartbeat), int(TriggerEvent), int(TriggerSelfWake))
|
||||
}
|
||||
if SelfWakeSource != "self_wake" {
|
||||
t.Fatalf("SelfWakeSource = %q, want %q(面板与文档按此值登记)", SelfWakeSource, "self_wake")
|
||||
}
|
||||
if SelfWakeSummaryPrefix != SelfWakeSource+": " {
|
||||
t.Fatalf("SelfWakeSummaryPrefix = %q, want %q", SelfWakeSummaryPrefix, SelfWakeSource+": ")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelfWakeTrigger_ShapeAndUndeclaredDefault 钉住自排轮的形状(C.1/C.2):来源与
|
||||
// 摘要用稳定标签、主题级默认是 Normal(不是 Low —— 不新造一档「比例行还低」的语义)、
|
||||
// Kind 是 TriggerSelfWake;零值声明不上抬。
|
||||
func TestSelfWakeTrigger_ShapeAndUndeclaredDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
tr := selfWakeTrigger(SelfWakeRequest{Reason: "想看看她今天的安排"})
|
||||
if tr.Source != SelfWakeSource {
|
||||
t.Fatalf("Source = %q, want %q", tr.Source, SelfWakeSource)
|
||||
}
|
||||
if tr.Kind != TriggerSelfWake {
|
||||
t.Fatalf("Kind = %v, want %v", tr.Kind, TriggerSelfWake)
|
||||
}
|
||||
if tr.Import != ImportanceNormal {
|
||||
t.Fatalf("Import = %v, want %v(未声明档位时不上抬)", tr.Import, ImportanceNormal)
|
||||
}
|
||||
if tr.Summary != SelfWakeSummaryPrefix+"想看看她今天的安排" {
|
||||
t.Fatalf("Summary = %q, want the stable prefix + 她写的理由", tr.Summary)
|
||||
}
|
||||
// 理由为空也要带前缀:context.go 靠前缀认轮次,不靠理由内容。
|
||||
if empty := selfWakeTrigger(SelfWakeRequest{}); !strings.HasPrefix(empty.Summary, SelfWakeSummaryPrefix) {
|
||||
t.Fatalf("empty-reason Summary = %q, want the stable prefix", empty.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSelfWakeRequest_DeclarerChannel 钉住自排**没有**第二条档位通道:她的声明经
|
||||
// urgency.Declarer 被 foldDeclared 读出(与 IoT 设备跃迁同一个接口),因此
|
||||
// SelfWakeRequest 必须满足该接口。
|
||||
func TestSelfWakeRequest_DeclarerChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
var d urgency.Declarer = SelfWakeRequest{Reason: "r", Declared: urgency.LevelWarning}
|
||||
if got := d.Urgency(); got != urgency.LevelWarning {
|
||||
t.Fatalf("Urgency() = %v, want %v", got, urgency.LevelWarning)
|
||||
}
|
||||
if got := (SelfWakeRequest{}).Urgency(); got != urgency.LevelRoutine {
|
||||
t.Fatalf("zero-value Urgency() = %v, want %v(未声明 = routine)", got, urgency.LevelRoutine)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackthink_SelfWakeRoundProductionPath 走生产路径 think():一轮自排思考落库为
|
||||
// trigger="self_wake",长期静默段里例常自排被扣下(C.4),而声明 warning 后放行且
|
||||
// 落库档位是**折算后**的 warning(C.6)。对应 C.8 判据 6 / 9。
|
||||
//
|
||||
// 变异:把 backthink.go 的 `Kind: tr.Kind` 改回传入常量(如 TriggerHeartbeat)⇒ 第一段
|
||||
// 的「例常自排被扣下」变红;把 SelfWakeSource 改回心跳值 ⇒ 落库断言变红。
|
||||
func TestBackthink_SelfWakeRoundProductionPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
pusher := &mockPusher{result: PushResult{Pushed: true, Route: RouteActiveClient}}
|
||||
b, _ := newBackthink(t, Config{}, Deps{
|
||||
Thinker: &mockThinker{text: "[我决定:push]\n我想再看看她今天的安排"},
|
||||
State: &mockState{state: UserState{}},
|
||||
Pusher: pusher,
|
||||
Recent: &fakeRecent{msgs: []history.Message{{
|
||||
Role: history.RoleUser,
|
||||
Content: "我先去忙了",
|
||||
CreatedAt: dayAt().Add(-3 * time.Hour).UnixMilli(),
|
||||
}}},
|
||||
Now: func() time.Time { return dayAt() },
|
||||
})
|
||||
|
||||
b.think(context.Background(), selfWakeTrigger(SelfWakeRequest{Reason: "复盘一下今天的对话"}))
|
||||
|
||||
if got := pusher.count(); got != 0 {
|
||||
t.Fatalf("pusher calls = %d, want 0(长期静默段里例常自排被扣下)", got)
|
||||
}
|
||||
recs, err := b.List(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(recs) != 1 {
|
||||
t.Fatalf("records = %+v, want exactly one self-wake record", recs)
|
||||
}
|
||||
if recs[0].Trigger != SelfWakeSource {
|
||||
t.Fatalf("Record.Trigger = %q, want %q", recs[0].Trigger, SelfWakeSource)
|
||||
}
|
||||
if recs[0].ShouldPush {
|
||||
t.Fatalf("record = %+v, want the routine self wake held", recs[0])
|
||||
}
|
||||
if !strings.Contains(recs[0].PushResult.Detail, "self-initiated wake") {
|
||||
t.Fatalf("PushResult.Detail = %q, want it to name the self-initiated wake hold", recs[0].PushResult.Detail)
|
||||
}
|
||||
|
||||
// 同一轮只换声明:warning ⇒ 折算 High ⇒ 长期静默段放行,且落库档位是折算后的值。
|
||||
b.think(context.Background(), selfWakeTrigger(SelfWakeRequest{
|
||||
Reason: "她可能正需要人陪", Declared: urgency.LevelWarning,
|
||||
}))
|
||||
|
||||
recs, err = b.List(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if !recs[0].ShouldPush || !recs[0].PushResult.Pushed {
|
||||
t.Fatalf("declared self wake = %+v, want a delivered push", recs[0])
|
||||
}
|
||||
if recs[0].Urgency != urgency.LevelWarning.String() {
|
||||
t.Fatalf("Record.Urgency = %q, want %q(落库的是该轮实际生效的档位)", recs[0].Urgency, urgency.LevelWarning.String())
|
||||
}
|
||||
if got := pusher.count(); got != 1 {
|
||||
t.Fatalf("pusher calls = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,13 @@ const (
|
||||
// 系统事件)。它的推送绕过 PushGate——"该发就发"——只受
|
||||
// 同一事件去重约束。
|
||||
TriggerEvent
|
||||
// TriggerSelfWake 是她自己排起的一轮思考(design/12 §N C)。它既不是外部
|
||||
// 事件、也不是周期性心跳:推送既不绕 PushGate,也不享长期静默段的豁免——
|
||||
// 要求只是她说得出理由(见 decide.go 的 activityHeld)。
|
||||
//
|
||||
// 它是**追加**的第三个值:既有两个值的编号与标签逐字不变(改编号会
|
||||
// 静默改写已落库记录与在途请求的含义)。
|
||||
TriggerSelfWake
|
||||
)
|
||||
|
||||
// String 返回用于日志与记录的稳定标签。
|
||||
@@ -28,6 +35,8 @@ func (k TriggerKind) String() string {
|
||||
switch k {
|
||||
case TriggerEvent:
|
||||
return "event"
|
||||
case TriggerSelfWake:
|
||||
return "self_wake"
|
||||
case TriggerHeartbeat:
|
||||
return "heartbeat"
|
||||
default:
|
||||
@@ -45,6 +54,17 @@ const (
|
||||
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、泛化的
|
||||
@@ -60,6 +80,23 @@ type SystemEvent struct {
|
||||
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 {
|
||||
@@ -90,19 +127,37 @@ func startupTrigger() Trigger {
|
||||
}
|
||||
|
||||
// triggerForEvent 把总线事件映射为 Trigger:主题给出可读摘要与主题级默认重要度,
|
||||
// 随后**通用通道**统一解出重要度 —— 任何事件载荷只要实现 urgency.Declarer
|
||||
// (IoT 的 DeviceUpdate 是第一个使用者),它声明的紧急度就被承认;泛化系统事件沿用
|
||||
// 它既有的 SystemEvent.Import 口径。两条形式最终都落在同一个 Trigger.Import 字段上,
|
||||
// 判定侧(decide.go)因此只认档位、不认生产者。每个事件都打上 TriggerEvent,
|
||||
// 因此其主动推送绕过心跳的 PushGate。
|
||||
func triggerForEvent(e bus.Event) Trigger {
|
||||
tr := triggerForSubject(e)
|
||||
if d, ok := e.Payload.(urgency.Declarer); ok {
|
||||
// 随后**通用通道**(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 {
|
||||
@@ -174,8 +229,11 @@ func urgencyForImportance(i Importance) urgency.Level {
|
||||
// 主题级默认(如提醒的 High)与安全域事件既得的档位不会被一句 routine 抹掉;
|
||||
// 未声明(零值 LevelRoutine)时原样返回 base,没标紧急度的事件行为逐字不变。
|
||||
//
|
||||
// 两个调用方共用它:总线事件的载荷声明(triggerForEvent),以及她自己在主动思考轮
|
||||
// 结论里声明「我认为这条紧急」(backthink.go 的 escalatedImport)。
|
||||
// 两条来源共用它:① foldDeclared —— 生产者声明的档位,它是唯一折算点,服务总线事件
|
||||
// (triggerForEvent)与她自排的一轮(selfWakeTrigger)两种触发;② backthink.go 的
|
||||
// escalatedImport —— 她自己在主动思考轮结论里声明「我认为这条紧急」。因此「工具参数
|
||||
// urgency」与「结论尾段 [紧急:…]」两个声明点落在同一个 Trigger.Import 上,
|
||||
// 该轮生效值取两者的较高档。
|
||||
func escalateImport(base Importance, declared urgency.Level) Importance {
|
||||
if declared == urgency.LevelRoutine {
|
||||
return base
|
||||
|
||||
@@ -63,6 +63,54 @@ func TestTriggerForEvent_GenericProducerChannel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFoldDeclared_SharesOneChannelWithSelfWake 是 design/12 §C.2 的结构断言:这条
|
||||
// 通用通道**只有一处**折算点(foldDeclared),总线事件载荷与她自排的一轮都经它叠档
|
||||
// ——同一个声明档位必须给出同一个 Import。它同时钉住「声明只上抬」(routine 不压低
|
||||
// 主题自带档)与自排轮的形状(Source / Summary 前缀 / Kind)。
|
||||
//
|
||||
// 变异:把 foldDeclared 改成空操作(直接 return tr)、或让自排另写一条「档位 → 重要度」
|
||||
// switch ⇒ warning / critical 两条用例变红。
|
||||
func TestFoldDeclared_SharesOneChannelWithSelfWake(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
name string
|
||||
declared urgency.Level
|
||||
want Importance
|
||||
}{
|
||||
{name: "未声明_不上抬", declared: urgency.LevelRoutine, want: ImportanceNormal},
|
||||
{name: "warning_抬到High", declared: urgency.LevelWarning, want: ImportanceHigh},
|
||||
{name: "critical_抬到Critical", declared: urgency.LevelCritical, want: ImportanceCritical},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ev := triggerForEvent(bus.Event{Subject: SubjectSystemEvent, Payload: stubProducer{Level: tc.declared}})
|
||||
if ev.Import != tc.want {
|
||||
t.Fatalf("event Import = %v, want %v", ev.Import, tc.want)
|
||||
}
|
||||
sw := selfWakeTrigger(SelfWakeRequest{Reason: "复盘一下今天的对话", Declared: tc.declared})
|
||||
if sw.Import != tc.want {
|
||||
t.Fatalf("self wake Import = %v, want %v (与事件同一条通道)", sw.Import, tc.want)
|
||||
}
|
||||
if sw.Kind != TriggerSelfWake {
|
||||
t.Fatalf("self wake Kind = %v, want %v", sw.Kind, TriggerSelfWake)
|
||||
}
|
||||
if sw.Source != SelfWakeSource {
|
||||
t.Fatalf("self wake Source = %q, want %q", sw.Source, SelfWakeSource)
|
||||
}
|
||||
if !strings.HasPrefix(sw.Summary, SelfWakeSummaryPrefix) {
|
||||
t.Fatalf("self wake Summary = %q, want the stable prefix %q", sw.Summary, SelfWakeSummaryPrefix)
|
||||
}
|
||||
})
|
||||
}
|
||||
// 声明只上抬,绝不压低主题自带的档:提醒的 High 不会被一句 routine 抹掉。
|
||||
reminder := triggerForEvent(bus.Event{Subject: SubjectReminderTrigger, Payload: stubProducer{Level: urgency.LevelRoutine}})
|
||||
if reminder.Import != ImportanceHigh {
|
||||
t.Fatalf("reminder Import = %v, want %v (routine 不得压低主题档位)", reminder.Import, ImportanceHigh)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecide_GenericProducerQuietHours 断言非 IoT 生产者的声明在安静时段同样生效:
|
||||
// critical 穿透,未声明照常扣下。判定侧不改,穿透靠的是事件真的走到了那一档。
|
||||
func TestDecide_GenericProducerQuietHours(t *testing.T) {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// 它是一条**通用通道**:任何事件生产者 —— IoT(第一个使用者,规则表见
|
||||
// internal/iot/safety.go)、其它外围插件、系统 / 能力模块 —— 都可以在发布到进程内
|
||||
// bus 的载荷上实现 Declarer,声明「这次事件有多紧急」。backthink 在唯一一处
|
||||
// (trigger.go 的 triggerForEvent)把该声明与既有的 SystemEvent.Import 口径一起折算
|
||||
// (trigger.go 的 foldDeclared —— 由 triggerForEvent 与 selfWakeTrigger 共用)把该声明
|
||||
// 与既有的 SystemEvent.Import 口径一起折算
|
||||
// 进 Trigger.Import,因此安全域(critical)事件能穿透安静时段与工作 / 忙碌
|
||||
// (design/12 §2 §6 安全域豁免,裁决 1.8 / 07.4.2),而 backthink 绝不需要认识任何
|
||||
// 生产者(IoT 或插件)的专有字段。
|
||||
|
||||
Reference in new issue
Block a user