Why: the completion doc assigns the Memory port to the library; today memory recall is a product-side interface assertion with no library surface, AgentSpec.MemoryScope has no consumer, and the model has no write-back path. Evidence: GOWORK=off go vet + go test -count=1 ./... green; TestLoopInjectsMemoryAndRegistersWrite (recall block lands after the system section, memory_write executes through the loop), TestEmptyRecallInjectsNothing (empty recall changes nothing), TestBlockRenderOrderingAndCaps (deterministic order, expiry/empty pruning), TestWriteToolValidation (fail-closed scope/type/content). Impact: new opt-in Config.Memory/MemoryScope; nil port keeps behavior byte-identical; injection is request-scoped (never persisted, never accumulates) and deterministic ([记忆] text shape preserved for the product migration). Refs: Agent-System-Documents#43 (N3 plan WP5)
97 lines
3.2 KiB
Go
97 lines
3.2 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"agent-native/runtime/llm"
|
|
"agent-native/runtime/memory"
|
|
"agent-native/runtime/tools"
|
|
)
|
|
|
|
// ---- N3 WP5:循环消费点(注入 + memory_write 接线)----
|
|
|
|
type loopMemStore struct {
|
|
entries []memory.Entry
|
|
appended []memory.Entry
|
|
}
|
|
|
|
func (s *loopMemStore) Recall(_ context.Context, q memory.RecallQuery) ([]memory.Entry, error) {
|
|
return s.entries, nil
|
|
}
|
|
func (s *loopMemStore) Append(_ context.Context, e memory.Entry) error {
|
|
s.appended = append(s.appended, e)
|
|
return nil
|
|
}
|
|
|
|
// 端口注入的条目出现在请求副本系统段之后;nil 端口零变化;
|
|
// Run 启动注册 memory_write 且可调用。
|
|
func TestLoopInjectsMemoryAndRegistersWrite(t *testing.T) {
|
|
store := &loopMemStore{entries: []memory.Entry{{Type: "fact", Content: "项目用 Go 1.27"}}}
|
|
reg := tools.NewRegistry(t.TempDir())
|
|
var gotReq [][]llm.Message
|
|
res, err := Run(context.Background(), Config{
|
|
Model: "fake", Tools: reg, Memory: store, MaxTurns: 2,
|
|
Stream: func(ctx context.Context, model string, msgs []llm.Message, _ []llm.ToolDecl, _ llm.StreamOptions) <-chan llm.Event {
|
|
gotReq = append(gotReq, append([]llm.Message{}, msgs...))
|
|
ch := make(chan llm.Event, 1)
|
|
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{
|
|
Role: llm.RoleAssistant, StopReason: llm.StopToolCalls,
|
|
ToolCalls: []llm.ToolCall{{ID: "c1", Name: "memory_write", Arguments: `{"type":"fact","content":"主人喜欢简洁"}`}},
|
|
}}
|
|
close(ch)
|
|
return ch
|
|
},
|
|
}, llm.Message{Role: llm.RoleUser, Content: "go"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = res
|
|
if len(gotReq) < 1 {
|
|
t.Fatalf("请求次数 = %d", len(gotReq))
|
|
}
|
|
req := gotReq[0]
|
|
// [记忆] 块在系统段之后、历史之前。
|
|
sysEnd := 0
|
|
for sysEnd < len(req) && req[sysEnd].Role == llm.RoleSystem {
|
|
sysEnd++
|
|
}
|
|
var found bool
|
|
for i := 0; i < sysEnd; i++ {
|
|
if strings.Contains(req[i].Content, "[记忆]") && strings.Contains(req[i].Content, "项目用 Go 1.27") {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("记忆块未注入系统段:%#v", req)
|
|
}
|
|
// memory_write 已被注册并执行(脚本化模型每轮都点名它 → 至少一次)。
|
|
if len(store.appended) < 1 || store.appended[0].Content != "主人喜欢简洁" {
|
|
t.Fatalf("memory_write 未执行:%+v", store.appended)
|
|
}
|
|
}
|
|
|
|
// 空召回:不产生 [记忆] 块(注入静默跳过);nil 端口同语义。
|
|
func TestEmptyRecallInjectsNothing(t *testing.T) {
|
|
reg := tools.NewRegistry(t.TempDir())
|
|
var gotReq []llm.Message
|
|
if _, err := Run(context.Background(), Config{
|
|
Model: "fake", Tools: reg, Memory: &loopMemStore{},
|
|
Stream: func(ctx context.Context, model string, msgs []llm.Message, _ []llm.ToolDecl, _ llm.StreamOptions) <-chan llm.Event {
|
|
gotReq = append([]llm.Message{}, msgs...)
|
|
ch := make(chan llm.Event, 1)
|
|
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{Role: llm.RoleAssistant, Content: "x", StopReason: llm.StopEnd}}
|
|
close(ch)
|
|
return ch
|
|
},
|
|
}, llm.Message{Role: llm.RoleUser, Content: "go"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, m := range gotReq {
|
|
if strings.Contains(m.Content, "[记忆]") {
|
|
t.Fatalf("空召回不得产生记忆块:%#v", gotReq)
|
|
}
|
|
}
|
|
}
|