refactor: pluginize text cleaning and tool NoMemory control

- SDK: ToolDef.NoMemory field, PluginSDK.RegisterTextCleaner/TextCleaners
- Registry: aggregate text cleaners from plugins, expose CleanText()
- Memory: replace hardcoded QQ regex CleanTemplateText with dynamic CleanText/SetTextCleaner
- StageHost: add ToolDef(name) lookup
- eventloop: check ToolDef.NoMemory before emitMemoryCandidate
- context/Prune: replace hardcoded agentcli/terminal source filter with ToolsUsed NoMemory check
- agentcli/cmd: mark tools with NoMemory: true
- main.go: wire memory.SetTextCleaner(pluginReg.CleanText)
This commit is contained in:
root
2026-07-24 14:49:08 +08:00
parent 40a46c7376
commit f4147f81d7
32 changed files with 2368 additions and 69 deletions

View File

@ -165,6 +165,11 @@ func New(cfg AgentConfig) *Agent {
cfg.DocStore.ReindexWithVectorizer(embedder)
}
rc := NewRelevanceContext(cfg.ContextSavePath, embedder)
if cfg.StageHost != nil {
rc.SetToolDefLookup(cfg.StageHost.ToolDef)
}
return &Agent{
id: cfg.ID,
startTime: time.Now(),
@ -175,7 +180,7 @@ func New(cfg AgentConfig) *Agent {
indexer: cfg.Indexer,
skills: cfg.Skills,
tracker: cfg.Tracker,
context: NewRelevanceContext(cfg.ContextSavePath, embedder),
context: rc,
systemPrompt: cfg.SystemPrompt,
ctx: ctx,
cancel: cancel,

View File

@ -13,6 +13,7 @@ import (
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
type ContextEvent struct {
@ -27,12 +28,13 @@ type ContextEvent struct {
const contextFlushInterval = 5 * time.Second
type RelevanceContext struct {
mu sync.Mutex
events []*ContextEvent
embedder *memory.StaticEmbedder
savePath string
saveTimer *time.Timer
dirty bool
mu sync.Mutex
events []*ContextEvent
embedder *memory.StaticEmbedder
savePath string
saveTimer *time.Timer
dirty bool
toolDefLookup func(name string) *sdk.ToolDef
}
func NewRelevanceContext(savePath string, embedder *memory.StaticEmbedder) *RelevanceContext {
@ -46,6 +48,12 @@ func NewRelevanceContext(savePath string, embedder *memory.StaticEmbedder) *Rele
return rc
}
func (c *RelevanceContext) SetToolDefLookup(fn func(name string) *sdk.ToolDef) {
c.mu.Lock()
defer c.mu.Unlock()
c.toolDefLookup = fn
}
func (c *RelevanceContext) load() {
data, err := os.ReadFile(c.savePath)
if err != nil {
@ -56,7 +64,7 @@ func (c *RelevanceContext) load() {
return
}
for _, evt := range events {
evt.Input = memory.CleanTemplateText(evt.Input)
evt.Input = memory.CleanText(evt.Input)
evt.Vector = c.computeVector(evt)
}
c.events = events
@ -65,11 +73,11 @@ func (c *RelevanceContext) load() {
func textForVector(evt *ContextEvent) string {
switch {
case evt.Source == "agent" && evt.Response != "":
return memory.CleanTemplateText(evt.Response)
return memory.CleanText(evt.Response)
case evt.Source == "cold_storage":
return memory.CleanTemplateText(evt.Input + " " + evt.Response)
return memory.CleanText(evt.Input + " " + evt.Response)
default:
return memory.CleanTemplateText(evt.Input)
return memory.CleanText(evt.Input)
}
}
@ -95,7 +103,7 @@ func (c *RelevanceContext) Append(evt ContextEvent) {
c.mu.Lock()
defer c.mu.Unlock()
evt.Input = memory.CleanTemplateText(evt.Input)
evt.Input = memory.CleanText(evt.Input)
evt.Vector = c.computeVector(&evt)
c.events = append(c.events, &evt)
@ -193,7 +201,7 @@ func (c *RelevanceContext) Prune(currentInput string, topK int, docStore *docume
if docStore != nil && len(archive) > 0 {
var filtered []scored
for _, s := range archive {
if s.event.Source == "agentcli" || s.event.Source == "terminal" {
if hasNoMemoryTool(s.event.ToolsUsed, c.toolDefLookup) {
continue
}
filtered = append(filtered, s)
@ -257,3 +265,15 @@ func (c *RelevanceContext) Len() int {
defer c.mu.Unlock()
return len(c.events)
}
func hasNoMemoryTool(toolsUsed []string, lookup func(string) *sdk.ToolDef) bool {
if lookup == nil {
return false
}
for _, name := range toolsUsed {
if def := lookup(name); def != nil && def.NoMemory {
return true
}
}
return false
}

View File

@ -6,6 +6,7 @@ import (
"time"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func newTestCtx() *RelevanceContext {
@ -237,6 +238,40 @@ func containsStr(s, substr string) bool {
return false
}
func TestHasNoMemoryTool(t *testing.T) {
host := NewStageHost()
host.RegisterTool("no_mem_tool", sdk.ToolDef{Name: "no_mem_tool", NoMemory: true}, nil)
host.RegisterTool("mem_tool", sdk.ToolDef{Name: "mem_tool"}, nil)
lookup := host.ToolDef
gotNil := hasNoMemoryTool([]string{"no_mem_tool"}, nil)
if gotNil {
t.Error("hasNoMemoryTool with nil lookup should return false")
}
tests := []struct {
name string
toolsUsed []string
want bool
}{
{"empty tools", nil, false},
{"no matching tool", []string{"unknown"}, false},
{"tool without NoMemory", []string{"mem_tool"}, false},
{"tool with NoMemory", []string{"no_mem_tool"}, true},
{"mixed tools, first is no_memory", []string{"no_mem_tool", "mem_tool"}, true},
{"mixed tools, last is no_memory", []string{"mem_tool", "no_mem_tool"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := hasNoMemoryTool(tt.toolsUsed, lookup)
if got != tt.want {
t.Errorf("hasNoMemoryTool(%v) = %v, want %v", tt.toolsUsed, got, tt.want)
}
})
}
}
func splitLines(s string) []string {
var lines []string
start := 0

View File

@ -205,7 +205,7 @@ func (a *Agent) processMediaInput(evt *agentIO.InputEvent) {
a.emitResponse(evt, response)
if !stageCtx.NoMemory {
if !stageCtx.NoMemory && !a.hasNoMemoryTool(toolsUsed) {
a.emitMemoryCandidate(evt.Source, fallback, response, toolsUsed)
}
}
@ -334,7 +334,7 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
a.emitResponse(evt, response)
if !stageCtx.NoMemory {
if !stageCtx.NoMemory && !a.hasNoMemoryTool(toolsUsed) {
a.emitMemoryCandidate(evt.Source, input, response, toolsUsed)
}
}
@ -386,6 +386,15 @@ func (a *Agent) emitResponse(evt *agentIO.InputEvent, response string) {
a.runStage(sdk.StageAfterOutput, stageCtx)
}
func (a *Agent) hasNoMemoryTool(toolsUsed []string) bool {
for _, name := range toolsUsed {
if def := a.stageHost.ToolDef(name); def != nil && def.NoMemory {
return true
}
}
return false
}
func (a *Agent) drainInterrupts() []string {
var out []string
for {

View File

@ -54,6 +54,17 @@ func (h *StageHost) GetToolDefs() []sdk.ToolDef {
return defs
}
func (h *StageHost) ToolDef(name string) *sdk.ToolDef {
h.mu.RLock()
defer h.mu.RUnlock()
for _, def := range h.toolDefs {
if def.Name == name {
return &def
}
}
return nil
}
func (h *StageHost) ExecuteTool(name string, args map[string]interface{}) (ret interface{}, err error) {
defer func() {
if r := recover(); r != nil {

View File

@ -197,3 +197,54 @@ func TestStageHostMultipleTools(t *testing.T) {
t.Errorf("expected from_p2, got %v", r2)
}
}
func TestStageHostToolDefLookup(t *testing.T) {
host := NewStageHost()
host.RegisterTool("tool_a", sdk.ToolDef{Name: "tool_a", NoMemory: true}, nil)
host.RegisterTool("tool_b", sdk.ToolDef{Name: "tool_b"}, nil)
def := host.ToolDef("tool_a")
if def == nil {
t.Fatal("expected tool_a to be found")
}
if !def.NoMemory {
t.Error("tool_a should have NoMemory=true")
}
def = host.ToolDef("tool_b")
if def == nil {
t.Fatal("expected tool_b to be found")
}
if def.NoMemory {
t.Error("tool_b should have NoMemory=false")
}
def = host.ToolDef("nonexistent")
if def != nil {
t.Errorf("expected nil for nonexistent tool, got %v", def)
}
}
func TestStageHostToolDefNoMemoryStored(t *testing.T) {
host := NewStageHost()
host.RegisterTool("mem_tool", sdk.ToolDef{Name: "mem_tool", NoMemory: true}, nil)
host.RegisterTool("normal_tool", sdk.ToolDef{Name: "normal_tool", NoMemory: false}, nil)
defs := host.GetToolDefs()
found := map[string]bool{}
for _, d := range defs {
found[d.Name] = d.NoMemory
}
if v, ok := found["mem_tool"]; !ok {
t.Error("mem_tool not found in defs")
} else if !v {
t.Error("mem_tool.NoMemory should be true")
}
if v, ok := found["normal_tool"]; !ok {
t.Error("normal_tool not found in defs")
} else if v {
t.Error("normal_tool.NoMemory should be false")
}
}