mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-03 15:57:35 +00:00
用户裁定的 L1 契约(2026-10-27):
· 保留 m 条(core.agent.max_context_size)、保护最新 n 条
(core.agent.context.protected_count)、其余按相似度淘汰 → 我的上一版把
m 写到了不存在的键(core.agent.context.max_context_size),运行时仍读
core.agent.max_context_size=200 ⇒ L1 堆到 119 条,工作记忆退化成历史堆。
· 送给模型的是**时序排列的对话** → 曾被渲染成"摘要 + 工具管道"
("—/ → 调用工具: a, b → 已回复…",助手回复还截 120 字),
指代消解因此失效(用户回"新乡",接不上"帮我找一套月租<400…")。
· 工具结果在 context 中**展开全文**,只有**超长**结果折叠成句柄
(toolresult_store + toolresult_read 按区间读回)→ 曾额外加 160/400 字、
每事件 3 条的截断,等于把已展开的结果又藏回去。
本次改动(仅渲染层,不动淘汰/归档/中断语义):
· formatMergedTimeline 渲染 [ts] 用户: … / [ts] 助手: …(回复全文)
· 中断轮的"用户原话"取自 qq_get_message/qq_get_history 的回执正文
(Input 是注入样板,不是人话)
· timelineToolLines:普通结果全文展开;ToolResultStores 命中的只给句柄
· 预算估算按实际渲染内容计(用户行 + 助手全文 + 工具行)
判据:internal/agent/core/timeline_contract_test.go
(时序对话/原话来源/全文展开/仅超长折叠)
99 lines
3.4 KiB
Go
99 lines
3.4 KiB
Go
package core
|
||
|
||
// L1(工作记忆区)送给模型的形态由用户裁定(2026-10-27):
|
||
//
|
||
// 1. **时序排列的对话**:用户行 / 助手行,助手回复全文(不截 120 字);
|
||
// 2. 中断轮的"用户原话"从聊天工具回执里取(Input 是注入样板,不是人话);
|
||
// 3. 工具结果**展开全文**,不截断、不限条数;
|
||
// 4. 只有**超长**结果(已落盘进 toolresult_store)折叠成句柄。
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/memory"
|
||
)
|
||
|
||
func newTimelineCtx() *RelevanceContext {
|
||
return NewRelevanceContext("", memory.NewStaticEmbedder(""))
|
||
}
|
||
|
||
func TestTimelineRendersTemporalDialogue(t *testing.T) {
|
||
rc := newTimelineCtx()
|
||
// 中断轮:Input 是样板,人话在 qq_get_message 的回执里
|
||
rc.Append(ContextEvent{
|
||
Timestamp: time.Date(2026, 10, 27, 16, 5, 44, 0, time.Local),
|
||
Source: "agent",
|
||
NoMemory: true,
|
||
Input: "来自「—/」的群消息(message_id=1)。先用qq_get_message取正文…",
|
||
Response: "好的,我去找月租低于400、水电常价、带独卫的单间。",
|
||
ToolResults: []ToolResultItem{
|
||
{Name: "qq_get_message", Output: `{"content":"[CQ:at,qq=1] 帮我找一套月租<400 水电常价带独卫的单间我就去"}`},
|
||
},
|
||
})
|
||
a := &Agent{context: rc}
|
||
out := a.formatMergedTimeline(4000)
|
||
|
||
if strings.Contains(out, "先用qq_get_message取正文") {
|
||
t.Fatalf("中断样板不得作为用户原话出现:\n%s", out)
|
||
}
|
||
if !strings.Contains(out, "用户: [CQ:at,qq=1] 帮我找一套月租<400 水电常价带独卫的单间我就去") {
|
||
t.Fatalf("用户行必须是聊天工具回执里的原话:\n%s", out)
|
||
}
|
||
if !strings.Contains(out, "助手: 好的,我去找月租低于400、水电常价、带独卫的单间。") {
|
||
t.Fatalf("助手行必须是完整回复(不截断):\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestTimelineExpandsToolResultsInFull(t *testing.T) {
|
||
rc := newTimelineCtx()
|
||
long := strings.Repeat("行内容", 200) // 1000 rune,远超旧 160/400 字截断
|
||
rc.Append(ContextEvent{
|
||
Timestamp: time.Now(),
|
||
Source: "agent",
|
||
Input: "查一下",
|
||
Response: "查到了。",
|
||
ToolResults: []ToolResultItem{
|
||
{Name: "cmd_run", Output: long},
|
||
},
|
||
})
|
||
a := &Agent{context: rc}
|
||
out := a.formatMergedTimeline(200000)
|
||
if !strings.Contains(out, long) {
|
||
t.Fatalf("普通工具结果必须展开全文(长度 %d)", len(long))
|
||
}
|
||
if strings.Contains(out, "…") && strings.Contains(out, "工具(cmd_run): 行内容行内容…") {
|
||
t.Fatal("不得对普通工具结果做截断")
|
||
}
|
||
}
|
||
|
||
func TestTimelineFoldsOnlyOversizedResults(t *testing.T) {
|
||
rc := newTimelineCtx()
|
||
rc.Append(ContextEvent{
|
||
Timestamp: time.Now(),
|
||
Source: "agent",
|
||
ID: "evt_1",
|
||
Input: "跑大查询",
|
||
Response: "结果太长,已落盘。",
|
||
ToolResults: []ToolResultItem{
|
||
{Name: "cmd_run", Output: "预览:..."},
|
||
{Name: "qq_get_message", Output: `{"content":"短回执"}`},
|
||
},
|
||
ToolResultStores: []ToolResultStore{
|
||
{Tool: "cmd_run", TotalBytes: 36783, TotalLines: 900},
|
||
},
|
||
})
|
||
a := &Agent{context: rc}
|
||
out := a.formatMergedTimeline(200000)
|
||
if !strings.Contains(out, `toolresult_read(event_id="evt_1")`) {
|
||
t.Fatalf("超长结果必须折叠成句柄:\n%s", out)
|
||
}
|
||
if strings.Contains(out, "预览:...") {
|
||
t.Fatalf("超长结果不得渲染正文:\n%s", out)
|
||
}
|
||
if !strings.Contains(out, "短回执") {
|
||
t.Fatalf("未超长的结果必须展开:\n%s", out)
|
||
}
|
||
}
|