Files
HomeAgent/internal/agent/core/memoryface.go
JianFeeeee aba1770701 fix(lightkernel): 传统上下文落到实处 —— 子不做策略性裁剪(并修掉裁剪估算的字节/rune 单位混用)
用户指出实现不自洽:"子 agent 是传统上下文,没有裁剪"。查证属实:子仍走
`formatMergedTimeline` 的预算裁剪、也可能走 `pruneOnInput`(既裁剪又向 doc 记忆归档)。
即"动态上下文"(父专属能力)漏进了轻量内核。

## 三处闸门(按 isLightKernel() 判)

1. **拼装预算**:新增 `contextTokenBudget(b)` —— 父用动态上下文的 `ContextTokens`,
   子用**整个窗口** `MaxContext`。两个调用点都改(`stepPrepare`、`rebaseFramePrefix`;
   漏掉后者时 prepare 之后的重建仍会裁,实测就是这么被抓出来的)。
2. **pruneOnInput**:轻量内核前置返回 —— 不做按相关度裁剪、不向 doc 记忆归档。
3. doc 记忆 / 整理流水线:早已由 `a.memory == nil` 关闭(N2c)。

## "不裁"的准确含义

不做**策略性**裁剪(不按相关度挑、不归档),只受"模型能收多少"这个硬上限约束;
且在撞到硬上限之前,contextfull(90% 窗口)已按 L4 上报父 agent ⇒
**丢事件的决定权在父**(压缩/回收/销毁),不在内核。文档 §16.0.0 记录三处闸门表。

## 顺带修掉的既有 bug(单位混用)

`formatMergedTimeline` 逐事件估算原来是 `len(e.Source)+len(e.Input)+40` 再 ×2 ——
`len()` 是**字节**,而 `EstimateTokens` 是 rune×2 ⇒ 中文事件被高估 3 倍:
实测 2384 字的中文事件被估成 14398 token > 8192,于是窗口还有余量也提前 break、
把更早的事件整段丢掉。改为统一走 `EstimateTokens`(对父同样生效:中文长会话不再被过早裁剪)。

## 测试

- 新增 `TestLightKernel_TraditionalContextNoTrimming`:用**记录模型实收消息**的 provider
  断言"更早的事件仍在"(为什么不看 TaskFrame:`prepareInputTask` 只做前半段,
  消息在 `runTaskSteps`/`stepPrepare` 才拼出来 —— 我第一版断言就打在了空帧上);
  填充量取"超过动态份额、但仍在窗口内",从而能区分父/子两种行为。
- 新增 `TestFullKernel_StillUsesDynamicContext` 作对照(父仍用动态份额)。

全量 go test ./... 37 包 ok / 0 FAIL;-race 干净。
2026-09-13 10:36:04 +08:00

57 lines
2.8 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 core
// 图记忆的**共同面**(设计 docs/zh/resident-subagent-design.md §16.0)。
//
// 只有两个方法 —— 因为按调用方实测分类后,`Agent.memory` 的 42 处使用里,
// 真正"根与子都要"的只有:
//
// Recall 读(上下文检索 / memory_recall
// Commit 写(自动写入路径的图部分 / memory_commit
//
// 其余 40 处全是**主 agent 整理记忆**与**记忆整理流水线**
// - 记忆整理流水线distill.go 的 archive/review/merge 循环
// - 记忆块 + 媒体桥graphmedia.go / medialoop.go
// - 记忆整理工具memory_merge / memory_delete_entity / memory_block_merge /
// memory_purge / memory_edit / memory_introspect
//
// 所以驻留子的轻量内核**不该有那些代码路径**:它用 `*memory.LightMemory` 接上 `graph`
// 而 `a.memory`(整理面)保持 nil —— 既有的 22 处 `if a.memory != nil` 关卡
// 会自动把整理面全部禁掉,不需要写"每个方法都返回错误"的受限包装。
import "gitcode.com/JianFeeeee/HomeAgent/internal/memory"
// isLightKernel 报告本 agent 是不是**轻量内核**(驻留子)。
//
// 轻量内核 = 传统上下文 + 图记忆(作用域化):它**没有**动态上下文能力
// (预算裁剪 / 按相关度裁剪并向 doc 记忆归档),那些是父 agent 专属的。
func (a *Agent) isLightKernel() bool { return a != nil && a.parentID != "" }
// contextTokenBudget 返回拼装时间线时可用的 token 预算。
//
// 完整内核:用动态上下文算出来的 ContextTokens按相关度/预算**策略性**裁时间线)
// 轻量内核:**用整个窗口** —— 传统上下文只受"模型能收多少"这个**硬上限**约束,
// 不做任何策略性裁剪(不按相关度挑、不向 doc 记忆归档);
// 而且在撞到硬上限之前contextfull90% 窗口)已按 L4 上报父 agent
// 决策(压缩/回收/销毁)—— 丢事件的决定权在父,不在内核。
func (a *Agent) contextTokenBudget(b TokenBudget) int {
if a.isLightKernel() {
if b.MaxContext > 0 {
return b.MaxContext
}
return defaultMaxContextTokens
}
return b.ContextTokens
}
// GraphMemory 是任意 agent 都能用的图记忆面。
//
// 实现者:
// - 根 agent直接就是 *memory.GraphDB
// - 驻留子:*memory.LightMemory读 tempmain只写 temp
type GraphMemory interface {
// Recall 按关键词/种子实体召回(子的实现是两空间并集)。
Recall(keywords []string, seedEntities []string, depth int, sessionFilter string) (*memory.RecallResult, error)
// Commit 写入三元组(子的实现只落自己的 temp 空间)。
Commit(triples []memory.Triple, sessionID string, turnID int) (int, int, error)
}