Files
weichen bdb45c3c2c
ci / vet-and-test (pull_request) Successful in 49s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Add the memory port with loop recall injection and the memory_write tool (N3 WP5)
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)
2026-10-07 16:05:36 +00:00

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)
}
}
}