feat(sdk+memory): 补 ScenePolicy 声明项,让通道能退出场面识别

缺口(R6):ChannelDef 的记忆声明已有三件套——NoMemory 管「进不进
记忆计算」、ContextPolicy 管「裁不裁上下文」、RecallPolicy 管「召不召回
记忆」,唯独没有「这条输入算不算一场戏的一部分」。现状是无条件参与:
situationFeaturesFor 里只要 evt.Source != "" 就产出一个 chan 特征,没有
可关的开关 ⇒ chan:system / chan:kernel / chan:timer 这类纯内部信噪通道
也在撑场面,每次触发都让不相干的场景长出来或变强,召回时又会把
「内核在跑定时器」当成「用户在这类场景下说过的话」取回。

穷举确认不是查漏:go.mod replace 指向 third_party/homeagent-sdk,
plugin.go 中 scene 出现 0 次,SDK 自身 git 历史 -S'Scene' -- sdk/ 为空。

SDK(纯追加,老插件行为逐字节不变):
- 常量 ScenePolicyAuto / ScenePolicyNone + ValidScenePolicy,形状与
  ContextPolicy / RecallPolicy 完全一致
- ChannelDef.ScenePolicy 与 InjectOptions.ScenePolicy,均带 omitempty
- 默认取 auto(参与)而非 none:场景只附加检索路、不改记忆本体,
  默认关会让存量通道突然失去召回;「关」是少数意图。与 ContextPolicy
  刻意相反(同为破坏性操作,那里是默认关)。

内核:
- applyInjectOpts 搬运 scene_policy(与另外三个标志位同面)
- sceneSuppressed 完全照 recallDeclared 的形状:注入点 payload >
  通道定义 > 默认。none 时连时段(part)特征都不产,也不派生场景键
  (只停指纹采集而留声明路,等于给这个口子开后门)
- situationFeaturesFor / sceneKeysFor 由包级函数改为 Agent 方法
  (需要 a.io 查通道定义),23 个调用点同步

判据:scenepolicy_test.go 7 例,改前编译期红(undefined:
pubsdk.ScenePolicyAuto),改后全绿。其中两例专门护住「未声明时行为
逐字节不变」,是纯追加承诺的护栏。

记忆 8 包 + agent/core 全绿,8 包齐全、无 FAIL/panic/race。

存量通道标注待定:kernel/timer/offload-*/child/* 是纯 0-refs 信噪,
可直接标 none;但 mc:system(12 refs) 与 system(3 refs) 带真实记忆,
性质不明,不擅自标。
This commit is contained in:
JianFeeeee
2026-09-26 20:09:17 +08:00
parent c7d1aa06d2
commit be9c1dc5ac
8 changed files with 241 additions and 18 deletions

View File

@ -7,6 +7,7 @@ import (
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
// sceneKeysFor 推导本轮输入的**当前场景**。
@ -19,8 +20,13 @@ import (
//
// 多个场景是**并列命中**(取回任一场景的记忆),不是交集:
// 「在 QQ 上」与「刚取回消息正文」是两个都能独立成立的触发条件。
func sceneKeysFor(evt *agentIO.InputEvent, toolName string) []string {
func (a *Agent) sceneKeysFor(evt *agentIO.InputEvent, toolName string) []string {
var keys []string
// 通道/注入点声明不参与场面识别时,**连派生场景键也不给**。
// 只停掉指纹采集而留着声明路,等于给「不参与场面」这个口子开了后门。
if a.sceneSuppressed(evt) {
return nil
}
seen := make(map[string]bool)
add := func(k string) {
// 显式声明的场景键来自插件,大小写/空白/标点都不可控;归一化后再去重,
@ -129,11 +135,41 @@ func (a *Agent) pruneByQuery(query string) int {
// 已有量,不需要模型配合,也不需要人工标注。
// ──────────────────────────────────────────────
// situationFeaturesFor 采集一轮交互的场面指纹。
// sceneSuppressed 报告本次输入是否被声明为**不参与场面识别**。
//
// 读取面与其它记忆声明完全一致:先看注入点 payload(单次覆盖),
// 再看通道定义(ChannelDef.ScenePolicy),都没声明 = 参与(保持既有行为)。
// 优先级与 pruneDeclared / recallDeclared 同构。
//
// 为什么要一个显式开关:场面指纹只要 evt.Source != "" 就无条件产出一个 chan
// 特征,于是内核自循环(system)、心跳(timer)、内部状态汇报(kernel)这类
// **纯信噪通道**也在撑场面——它们每次触发都让一个不相干的场景长出来或变强,
// 而召回时又会把「内核在跑定时器」当成「用户在这类场景下说过的话」取回。
func (a *Agent) sceneSuppressed(evt *agentIO.InputEvent) bool {
if evt == nil {
return false
}
if p, ok := evt.Payload["scene_policy"].(string); ok && p != "" {
return p == pubsdk.ScenePolicyNone
}
if a.io != nil {
if chDef, ok := a.io.GetInputChannelDef(evt.Source); ok && chDef.ScenePolicy != "" {
return chDef.ScenePolicy == pubsdk.ScenePolicyNone
}
}
return false
}
// sceneFeaturesFor 采集一轮交互的场面指纹。
//
// 特征权重由种类决定(见 memory.SituationFeature.Weight):通道与对象是
// 「同一个场面」最强的同一性信号,工具是行为信号,话题是软信号。
func situationFeaturesFor(evt *agentIO.InputEvent, cleanInput, tool string) []memory.SituationFeature {
func (a *Agent) situationFeaturesFor(evt *agentIO.InputEvent, cleanInput, tool string) []memory.SituationFeature {
// 声明不参与场面识别:连时段特征都不产——一个不参与的面孔
// 不该在 situation_evidence / scene_features 里留下任何足迹。
if a.sceneSuppressed(evt) {
return nil
}
var feats []memory.SituationFeature
if evt != nil {
if evt.Source != "" {
@ -225,8 +261,8 @@ func (a *Agent) resolveTurnScenes(f *TaskFrame, tool string) memory.TurnScene {
return f.turnScene
}
declared := sceneKeysFor(evtOf(f), tool)
feats := situationFeaturesFor(evtOf(f), cleanInputOf(f), tool)
declared := a.sceneKeysFor(evtOf(f), tool)
feats := a.situationFeaturesFor(evtOf(f), cleanInputOf(f), tool)
sig := memory.NewSituation(feats...)
turn, err := a.memory.EnterSceneWithHint(sig, declared)