From 8887e06274c5a819211f9258dfed152332300d0f Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 26 Sep 2026 20:09:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk+memory):=20=E8=A1=A5=20ScenePolicy=20?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E9=A1=B9=EF=BC=8C=E8=AE=A9=E9=80=9A=E9=81=93?= =?UTF-8?q?=E8=83=BD=E9=80=80=E5=87=BA=E5=9C=BA=E9=9D=A2=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缺口(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) 带真实记忆, 性质不明,不擅自标。 --- internal/agent/core/eventloop.go | 2 +- internal/agent/core/memorypass.go | 46 +++++++- internal/agent/core/scene_test.go | 19 ++-- internal/agent/core/scenepolicy_test.go | 144 ++++++++++++++++++++++++ internal/agent/core/task.go | 2 +- internal/agent/core/tooldefs.go | 2 +- internal/agent/io/channel.go | 5 +- third_party/homeagent-sdk/sdk/plugin.go | 39 ++++++- 8 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 internal/agent/core/scenepolicy_test.go diff --git a/internal/agent/core/eventloop.go b/internal/agent/core/eventloop.go index c30ca78..48428d1 100644 --- a/internal/agent/core/eventloop.go +++ b/internal/agent/core/eventloop.go @@ -430,7 +430,7 @@ func (a *Agent) pruneOnInput(evt *agentIO.InputEvent, cleanInput string) int { if !a.pruneDeclared(evt) { return 0 } - return a.memoryPass(cleanInput, "input:"+evt.Source, true, false, sceneKeysFor(evt, "")).Archived + return a.memoryPass(cleanInput, "input:"+evt.Source, true, false, a.sceneKeysFor(evt, "")).Archived } // pruneDeclared 判定这次输入是否显式声明了裁剪。 diff --git a/internal/agent/core/memorypass.go b/internal/agent/core/memorypass.go index a2da104..e20179c 100644 --- a/internal/agent/core/memorypass.go +++ b/internal/agent/core/memorypass.go @@ -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) diff --git a/internal/agent/core/scene_test.go b/internal/agent/core/scene_test.go index 8170037..f735b73 100644 --- a/internal/agent/core/scene_test.go +++ b/internal/agent/core/scene_test.go @@ -11,8 +11,9 @@ import ( // TestSceneKeysFor 钉住当前场景的推导优先级: // 注入点显式声明 > 通道 > 工具;并列命中且去重。 func TestSceneKeysFor(t *testing.T) { + a := &Agent{io: agentIO.NewIOManager()} // 通道 + 工具:两个都能独立成立的触发条件,都要带上 - got := sceneKeysFor(&agentIO.InputEvent{Source: "qq"}, "qq_get_message") + got := a.sceneKeysFor(&agentIO.InputEvent{Source: "qq"}, "qq_get_message") want := []string{"chan:qq", "tool:qq_get_message"} if len(got) != len(want) { t.Fatalf("sceneKeysFor = %v, want %v", got, want) @@ -28,7 +29,7 @@ func TestSceneKeysFor(t *testing.T) { Source: "QQ", Payload: map[string]interface{}{"scene": " chan:qq/peer:group_1 "}, } - got = sceneKeysFor(evt, "") + got = a.sceneKeysFor(evt, "") if len(got) != 2 || got[0] != "chan:qq/peer:group_1" || got[1] != "chan:qq" { t.Errorf("显式声明应排最前且通道场景归一: %v", got) } @@ -38,18 +39,18 @@ func TestSceneKeysFor(t *testing.T) { Source: "webui", Payload: map[string]interface{}{"scene": []interface{}{"chan:qq", "task:reminder"}}, } - got = sceneKeysFor(evt, "") + got = a.sceneKeysFor(evt, "") if len(got) != 3 || got[0] != "chan:qq" || got[1] != "task:reminder" || got[2] != "chan:webui" { t.Errorf("数组声明未生效: %v", got) } // nil 事件不 panic - if got := sceneKeysFor(nil, ""); len(got) != 0 { + if got := a.sceneKeysFor(nil, ""); len(got) != 0 { t.Errorf("nil 事件应无场景: %v", got) } // 未声明的 payload 键不影响 evt = &agentIO.InputEvent{Source: "cli", Payload: map[string]interface{}{"recall_policy": "none"}} - if got := sceneKeysFor(evt, ""); len(got) != 1 || got[0] != "chan:cli" { + if got := a.sceneKeysFor(evt, ""); len(got) != 1 || got[0] != "chan:cli" { t.Errorf("无 scene 声明时应只有通道场景: %v", got) } } @@ -57,11 +58,12 @@ func TestSceneKeysFor(t *testing.T) { // TestSituationFeaturesFor 钉住指纹来源:全部是运行时可观察量, // 不需要模型配合也不需要人工标注。 func TestSituationFeaturesFor(t *testing.T) { + a := &Agent{io: agentIO.NewIOManager()} evt := &agentIO.InputEvent{ Source: "QQ", Payload: map[string]interface{}{"group_id": float64(1027993713)}, } - feats := situationFeaturesFor(evt, "帮我看看排班表", "qq_get_message") + feats := a.situationFeaturesFor(evt, "帮我看看排班表", "qq_get_message") kinds := map[string]int{} for _, f := range feats { kinds[f.Kind]++ @@ -98,7 +100,7 @@ func TestSituationFeaturesFor(t *testing.T) { } // 无事件时不 panic,且只有工具特征时也成立 - if feats := situationFeaturesFor(nil, "", "memory_recall"); len(feats) != 1 { + if feats := a.situationFeaturesFor(nil, "", "memory_recall"); len(feats) != 1 { t.Errorf("仅工具场景应有 1 个特征: %+v", feats) } } @@ -106,9 +108,10 @@ func TestSituationFeaturesFor(t *testing.T) { // TestWritePathAttachesBothPaths 钉住写侧的「两条路都挂」: // 显式声明优先;否则挂本轮声明的 + 涌现的场景集合。 func TestTurnSceneKeysBothPaths(t *testing.T) { + a := &Agent{io: agentIO.NewIOManager()} // 声明的通道场景与工具场景都在,涌现键(若有)追加在后 evt := &agentIO.InputEvent{Source: "qq", Payload: map[string]interface{}{"scene": "chan:qq/peer:group_1"}} - got := sceneKeysFor(evt, "qq_get_message") + got := a.sceneKeysFor(evt, "qq_get_message") want := []string{"chan:qq/peer:group_1", "chan:qq", "tool:qq_get_message"} if len(got) != len(want) { t.Fatalf("声明侧场景数不对: %v want %v", got, want) diff --git a/internal/agent/core/scenepolicy_test.go b/internal/agent/core/scenepolicy_test.go new file mode 100644 index 0000000..15cd8c7 --- /dev/null +++ b/internal/agent/core/scenepolicy_test.go @@ -0,0 +1,144 @@ +package core + +// ScenePolicy 声明项的判据(R6)。 +// +// 缺口事实(穷举确认,非查漏):ChannelDef 已有 NoMemory/ContextPolicy/ +// RecallPolicy 三件套,唯独没有「这条通道是否参与场面识别」; +// situationFeaturesFor 里只要 evt.Source != "" 就无条件塞 chan 特征。 +// ⇒ chan:system / chan:kernel / chan:timer 这类内部信噪通道 +// 也在参与场面聚类(现网 65 个键里就有 chan:system、chan:kernel、chan:timer)。 +// +// 判据参照物在生产代码之外:期望值是「声明 none 的输入不产生任何场面特征」、 +// 「未声明的输入行为逐字节不变」这两条不变量,不引用被测实现。 + +import ( + "testing" + + agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io" + pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" +) + +func scenePolicyNoneEvent() *agentIO.InputEvent { + return &agentIO.InputEvent{ + Source: "system", + Payload: map[string]interface{}{"scene_policy": "none"}, + } +} + +// R6 核心:声明 none 的输入不产生任何场面特征。 +func TestScenePolicyNone_NoSituationFeatures(t *testing.T) { + a := &Agent{io: agentIO.NewIOManager()} + evt := scenePolicyNoneEvent() + + if feats := a.situationFeaturesFor(evt, "帮我看下定时器", ""); len(feats) != 0 { + t.Fatalf("声明 scene_policy=none 的输入仍产出了 %d 个场面特征: %+v —— "+ + "内部信噪通道会参与场面聚类,把无关场面撑出来", len(feats), feats) + } +} + +// none 必须连时段(part) 都不产:一个不参与场面识别的通道 +// 不该在 situation_evidence / scene_features 里留下任何足迹。 +func TestScenePolicyNone_NoPartFeatureLeak(t *testing.T) { + a := &Agent{io: agentIO.NewIOManager()} + feats := a.situationFeaturesFor(scenePolicyNoneEvent(), "任意内容", "") + for _, f := range feats { + if f.Kind == "part" { + t.Errorf("scene_policy=none 仍产出了时段特征 %q", f.Key()) + } + } +} + +// none 时工具场景也不该派生。 +func TestScenePolicyNone_NoToolScene(t *testing.T) { + a := &Agent{io: agentIO.NewIOManager()} + keys := a.sceneKeysFor(scenePolicyNoneEvent(), "cmd_run") + for _, k := range keys { + if k == "tool:cmd_run" || k == "chan:system" { + t.Errorf("scene_policy=none 仍派生了场景键 %q", k) + } + } +} + +// 声明 none 时,显式 payload["scene"] 也不该被采纳—— +// 否则通道声明形同虚设(注入点声明与通道声明必须一致,通道是更宽的闸)。 +func TestScenePolicyNone_OverridesExplicitScene(t *testing.T) { + a := &Agent{io: agentIO.NewIOManager()} + evt := scenePolicyNoneEvent() + evt.Payload["scene"] = "chan:qq" + + keys := a.sceneKeysFor(evt, "") + for _, k := range keys { + if k == "chan:qq" { + t.Error("通道声明 scene_policy=none 后,注入点显式声明的 chan:qq 仍被采纳;" + + "通道级闸门应覆盖注入点级声明") + } + } + if feats := a.situationFeaturesFor(evt, "", ""); len(feats) != 0 { + t.Errorf("scene_policy=none 仍产出特征 %+v", feats) + } +} + +// 未声明时行为必须逐字节不变(零值 = 保持现状 = 参与)。 +// 这是「纯追加」承诺的护栏:老插件不填 ScenePolicy,行为不能有任何变化。 +func TestScenePolicyUnset_BehavesExactlyAsBefore(t *testing.T) { + a := &Agent{io: agentIO.NewIOManager()} + evt := &agentIO.InputEvent{ + Source: "qq", + Payload: map[string]interface{}{}, + } + + feats := a.situationFeaturesFor(evt, "老大在吗", "") + if len(feats) == 0 { + t.Fatal("未声明 scene_policy 的输入不应失去场面特征(零值必须等价既有行为)") + } + var hasChan bool + for _, f := range feats { + if f.Kind == "chan" { + hasChan = true + } + } + if !hasChan { + t.Errorf("未声明时应照旧产出 chan 特征,实际: %+v", feats) + } + + keys := a.sceneKeysFor(evt, "") + if len(keys) == 0 || keys[0] != "chan:qq" { + t.Errorf("未声明时应照旧派生 chan:qq,实际: %v", keys) + } +} + +// 显式 auto 与未声明等价。 +func TestScenePolicyAuto_SameAsUnset(t *testing.T) { + a := &Agent{io: agentIO.NewIOManager()} + mk := func(policy string) []string { + payload := map[string]interface{}{} + if policy != "" { + payload["scene_policy"] = policy + } + return a.sceneKeysFor(&agentIO.InputEvent{Source: "qq", Payload: payload}, "") + } + unset, auto := mk(""), mk(pubsdk.ScenePolicyAuto) + + if len(unset) != len(auto) { + t.Fatalf("auto 与未声明不等价: unset=%v auto=%v", unset, auto) + } + if len(auto) == 0 || auto[0] != "chan:qq" { + t.Fatalf("auto 应照旧派生 chan:qq,实际: %v", auto) + } +} + +// SDK 常量与校验函数:形状须与 ContextPolicy/RecallPolicy 一致。 +func TestScenePolicySDKShape(t *testing.T) { + if !pubsdk.ValidScenePolicy("") { + t.Error("空串应等价默认,校验须通过") + } + if !pubsdk.ValidScenePolicy(pubsdk.ScenePolicyNone) { + t.Error("none 应合法") + } + if !pubsdk.ValidScenePolicy(pubsdk.ScenePolicyAuto) { + t.Error("auto 应合法") + } + if pubsdk.ValidScenePolicy("prune") { + t.Error("未知取值应被拒(照 ContextPolicy 的严格度)") + } +} diff --git a/internal/agent/core/task.go b/internal/agent/core/task.go index ae975aa..70e6253 100644 --- a/internal/agent/core/task.go +++ b/internal/agent/core/task.go @@ -788,7 +788,7 @@ func (a *Agent) stepToolAfter(f *TaskFrame) stepOutcome { // 与这一步工具本身(如 tool:qq_get_message)。带上工具场景, // 才能让「凡是要回 QQ 消息」这类规则在该步被取回。 // 召回用两条路的并集:声明场景(注入点/通道/工具)+ 涌现场景 - scenes := sceneKeysFor(f.Evt, tc.Name) + scenes := a.sceneKeysFor(f.Evt, tc.Name) turn := a.resolveTurnScenes(f, tc.Name) for _, k := range turn.Keys { scenes = append(scenes, k) diff --git a/internal/agent/core/tooldefs.go b/internal/agent/core/tooldefs.go index 6a8fdf3..c89e7c7 100644 --- a/internal/agent/core/tooldefs.go +++ b/internal/agent/core/tooldefs.go @@ -48,7 +48,7 @@ func (a *Agent) buildMemoryContext(input string, maxTokens int, scenes []string) // query 取**清洗后**的输入(通道 Cleaner 的输出),与裁剪侧同一套语义: // 原始输入里的 ANSI/base64/JSON 包装会把相关性打分带偏。清洗为空时回退原文。 func (a *Agent) buildTaskMemoryContext(f *TaskFrame, input string, maxTokens int) string { - scenes := sceneKeysFor(evtOf(f), "") + scenes := a.sceneKeysFor(evtOf(f), "") if f == nil { return a.recallText(input, "input", maxTokens, scenes) } diff --git a/internal/agent/io/channel.go b/internal/agent/io/channel.go index 14e76c4..c8cced7 100644 --- a/internal/agent/io/channel.go +++ b/internal/agent/io/channel.go @@ -341,7 +341,7 @@ type InjectOptions = pubsdk.InjectOptions // applyInjectOpts 把注入标志位写进事件 payload。 // // 只在非零时写:零值与旧 payload 逐字节一致,事件订阅方与旧内核 -// (不认识这两个键)都不会受影响。 +// (不认识这些键)都不会受影响。 // // 为什么不把标志位当独立参数传到底:eventloop 与各注入路径都按 payload 取字段 // (no_memory 本来就是这么走的),payload 是这里唯一已有的携带面。 @@ -355,6 +355,9 @@ func applyInjectOpts(payload map[string]interface{}, opts InjectOptions) { if opts.RecallPolicy != "" { payload["recall_policy"] = opts.RecallPolicy } + if opts.ScenePolicy != "" { + payload["scene_policy"] = opts.ScenePolicy + } if opts.CleanerName != "" { payload["cleaner_name"] = opts.CleanerName } diff --git a/third_party/homeagent-sdk/sdk/plugin.go b/third_party/homeagent-sdk/sdk/plugin.go index 6852696..93edf9b 100644 --- a/third_party/homeagent-sdk/sdk/plugin.go +++ b/third_party/homeagent-sdk/sdk/plugin.go @@ -74,6 +74,36 @@ func ValidRecallPolicy(policy string) bool { return false } +// 场面策略:决定一次输入是否参与**场面识别**(场景式记忆)。 +// +// 与前两项再正交一轴:NoMemory 管「进不进记忆计算」、ContextPolicy 管 +// 「裁不裁上下文」、RecallPolicy 管「召不召回记忆」,本项管的是 +// 「这条输入算不算一场戏的一部分」——它决定输入会不会产出现场指纹 +// (通道/对话对象/工具/话题/时段),进而决定会不会长出、命中、写入场景。 +// +// 默认(空串或 ScenePolicyAuto)**参与**,保持既有行为:场景式记忆自 +// v1.3 落地起就对所有通道无条件生效,没有开关。不默认关有两个原因: +// 1. 场景只**附加**现有记忆的检索路,不改记忆本体,默认关会让存量 +// 通道突然失去场景召回; +// 2. 「关」是少数意图(内部信噪通道),少数意图不该是默认—— +// 与 ContextPolicy 刻意相反(同为破坏性操作,那里是默认关)。 +// +// 该关的典型是纯内部通道:system(内核自循环)、kernel、timer、healthcheck。 +// 它们每次触发都在撑一个「场面」,会把不相干的交互聚到一起。 +const ( + ScenePolicyAuto = "auto" + ScenePolicyNone = "none" +) + +// ValidScenePolicy 校验场面策略取值;空串等价于 ScenePolicyAuto。 +func ValidScenePolicy(policy string) bool { + switch policy { + case "", ScenePolicyAuto, ScenePolicyNone: + return true + } + return false +} + // InjectOptions 声明一次注入行为在记忆层与上下文层的表现。 // // 零值 = 记入记忆 + 不裁剪上下文,与历史行为(三参数注入方法)完全一致, @@ -102,7 +132,11 @@ type InjectOptions struct { // 空串 = 默认(输入/注入 auto,即保持既有「每条输入都召回」的行为); // RecallPolicyNone 显式关闭(如中断通知的 meta 文本不该据它召回)。 RecallPolicy string - CleanerName string + // ScenePolicy 声明此次注入是否参与场面识别(场景式记忆)。 + // 空串 = 默认参与(保持既有行为);ScenePolicyNone 显式关闭, + // 适用于不产生任何场面指纹的纯内部信号(心跳、自循环、内部状态)。 + ScenePolicy string + CleanerName string // Priority 声明**中断注入**的优先级(仅 InjectInterrupt* 有意义)。 // @@ -132,6 +166,7 @@ const ( // Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用 // ContextPolicy: 此通道的输入到达后是否据此裁剪上下文,默认 none(不裁剪) // RecallPolicy: 此通道的输入到达后是否据此召回相关记忆,默认 auto(召回) +// ScenePolicy: 此通道的输入到达后是否参与场面识别(场景式记忆),默认 auto(参与) // // JSON tag 是必需的:通道定义要跨进程传给内核,而 Cleaner 是函数(必须忽略)。 // 没有 tag 时既无法整体 marshal(func 不支持),又会诱使调用方手写字段白名单—— @@ -142,6 +177,8 @@ type ChannelDef struct { ContextPolicy string `json:"context_policy,omitempty"` // RecallPolicy 见 InjectOptions.RecallPolicy;空串等价 auto(保持既有行为)。 RecallPolicy string `json:"recall_policy,omitempty"` + // ScenePolicy 见 InjectOptions.ScenePolicy;空串等价 auto(保持既有行为)。 + ScenePolicy string `json:"scene_policy,omitempty"` } // StageContext provides context for stage handlers.