diff --git a/internal/agent/core/context.go b/internal/agent/core/context.go index 5be10da2..dfd803b8 100644 --- a/internal/agent/core/context.go +++ b/internal/agent/core/context.go @@ -410,6 +410,15 @@ func (c *RelevanceContext) Save() error { // (另一处 InsertByTimestamp 同样处理)。所以明文拦截放在这里, // 而不是在每个调用点 —— 逐个补必漏(实测踩到:只拦了 f.Msgs, // 而 f.CleanInput / f.ToolResults / f.Response 都是独立副本)。 +// snapshotEvents 返回当前事件的浅拷贝(判据/诊断用;调用方不得修改元素)。 +func (c *RelevanceContext) snapshotEvents() []*ContextEvent { + c.mu.Lock() + defer c.mu.Unlock() + out := make([]*ContextEvent, len(c.events)) + copy(out, c.events) + return out +} + func (c *RelevanceContext) Append(evt ContextEvent) { c.mu.Lock() defer c.mu.Unlock() diff --git a/internal/agent/core/nomemory_flag_test.go b/internal/agent/core/nomemory_flag_test.go new file mode 100644 index 00000000..295460b3 --- /dev/null +++ b/internal/agent/core/nomemory_flag_test.go @@ -0,0 +1,61 @@ +package core + +// 插件的 `NoMemory` 声明必须落到**事件**上(2026-10-27)。 +// +// 声明链:InjectOptions.NoMemory / ChannelDef.NoMemory +// → payload["no_memory"] → stageCtx.NoMemory → ContextEvent.NoMemory +// +// 此前最后一步从来没发生:`ContextEvent.NoMemory` 只有在 coldcontext_query +// 回填时才被置 true,于是 QQ 那句取件样板 +//("来自「X」的私聊消息(message_id=…) 先用 qq_get_message 取正文…") +// 每轮都进计算层(向量化 / 关键词 / 蒸馏)。 +// +// 判据(按字段既有契约,不是新增语义): +// ① NoMemory 事件的 textForVector 为空(不进计算层); +// ② 但事件**照常归档**(Input/Response 原样保留,能从 L1 落回 L2); +// ③ 非 NoMemory 事件不受影响。 + +import ( + "testing" + "time" + + "github.com/JianFeeeee/HologramAgent/internal/memory" +) + +func newNoMemFlagContext() *RelevanceContext { + return NewRelevanceContext("", memory.NewStaticEmbedder("")) +} + +func TestNoMemoryEventSkipsComputationButKeepsArchiveFields(t *testing.T) { + rc := newNoMemFlagContext() + ev := ContextEvent{ + Timestamp: time.Now(), + Source: "agent", + NoMemory: true, + Input: "来自「—/」的私聊消息(message_id=1)。先用qq_get_message取正文", + Response: "收到。", + } + rc.Append(ev) + + got := rc.snapshotEvents() + if len(got) != 1 { + t.Fatalf("应写入一条事件,实际 %d", len(got)) + } + // ① 计算层:不进向量 + if txt := textForVector(got[0], nil, nil); txt != "" { + t.Fatalf("NoMemory 事件不得进入计算层,实际 textForVector=%q", txt) + } + // ② 归档字段保留(否则 L2 回放闭环断掉) + if got[0].Input == "" || got[0].Response == "" { + t.Fatalf("归档字段必须保留,实际 Input=%q Response=%q", got[0].Input, got[0].Response) + } +} + +func TestNormalEventStillEntersComputation(t *testing.T) { + rc := newNoMemFlagContext() + rc.Append(ContextEvent{Timestamp: time.Now(), Source: "webui", Input: "帮我看下日志"}) + got := rc.snapshotEvents() + if txt := textForVector(got[0], nil, nil); txt == "" { + t.Fatal("普通事件必须进入计算层") + } +} diff --git a/internal/agent/core/task.go b/internal/agent/core/task.go index adf6b392..01a593d6 100644 --- a/internal/agent/core/task.go +++ b/internal/agent/core/task.go @@ -558,9 +558,22 @@ func (a *Agent) finishInputTask(f *TaskFrame, out stepOutcome) { // 本轮捕获的媒体一起挂到这条事件上:用户上传的、插件注入的,以及模型调 // multimodal_see_picture / see_video 时经 SetToolBlocks 注入的。 + // ★ 把插件声明落到事件上(2026-10-27):`NoMemory` 只影响**计算层** + //(不向量化 / 不提关键词 / 不蒸馏)与**记忆视图**,而**归档照常** —— + // 这是 ContextEvent.NoMemory 的既有契约(见 context.go 的字段注释: + // coldcontext_query 的 Consume 已从 L2 删掉原文,回填事件必须能落回 L2, + // 否则三层都不在,内容被永久丢弃)。 + // + // 此前这个位从来没被赋值:插件声明 InjectOptions.NoMemory=true(QQ 那句 + // "来自「X」的私聊消息(message_id=…) 先用 qq_get_message 取正文…"), + // 经 payload.no_memory 合进 stageCtx.NoMemory 后,却只有 + // emitMemoryCandidate 一处消费它 —— 于是这句样板每轮都进计算层。 + // + // 声明来源:payload["no_memory"](插件注入)或 ChannelDef.NoMemory(通道)。 turnEvt := ContextEvent{ Timestamp: time.Now(), Source: "agent", + NoMemory: f.StageCtx.NoMemory, Input: f.CleanInput, Response: f.Response, ToolsUsed: f.ToolsUsed,