Files
Agent-Native/runtime/memory/memory.go
T
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

84 lines
2.2 KiB
Go

// Package memory 定义记忆端口(N3 WP5)。宿主注入 Store 实现;循环在每次
// 请求前把 Recall 结果以系统消息注入请求副本(不落盘、不累计——与
// reminders 同款的 request-scoped 语义);nil = 不启用。
//
// 注入文案与产品既有 [记忆] 形态对齐(迁移前后同文):
//
// [记忆]
// - (typ) content
//
// 排序确定性:按 (Type, Content) 字典序(同一 Recall 结果集在任何
// 消费方下渲染一致)。
package memory
import (
"context"
"fmt"
"sort"
"strings"
"time"
)
// Entry 是一条记忆。Scope 为 user 或 project。
type Entry struct {
ID string
Scope string
ProjectID string
Type string
Content string
Source string
Confidence float64
CreatedAt time.Time
ExpiresAt *time.Time
}
// RecallQuery 是一次召回。Scope 空 = 不限(含 user/project);Limit ≤0 = 20。
type RecallQuery struct {
Scope string
Limit int
}
// Store 是记忆端口。实现方自担 owner/项目上下文(循环不传租户态)。
type Store interface {
Recall(ctx context.Context, q RecallQuery) ([]Entry, error)
Append(ctx context.Context, e Entry) error
}
// MaxRecallEntries 是注入的条目上限(与产品既有形态一致)。
const MaxRecallEntries = 20
// Block 把一次 Recall 结果渲染成注入文本;空结果返回空串(调用方不注入)。
// 过期与空内容条目在渲染时剔除(与产品既有 InjectMemory 同语义)。
func Block(entries []Entry, now time.Time) string {
live := make([]Entry, 0, len(entries))
for _, e := range entries {
if e.ExpiresAt != nil && !e.ExpiresAt.After(now) {
continue
}
if strings.TrimSpace(e.Content) == "" {
continue
}
live = append(live, e)
}
if len(live) == 0 {
return ""
}
sort.Slice(live, func(i, j int) bool {
if live[i].Type != live[j].Type {
return live[i].Type < live[j].Type
}
return live[i].Content < live[j].Content
})
var b strings.Builder
b.WriteString("[记忆]\n")
n := 0
for _, e := range live {
fmt.Fprintf(&b, "- (%s) %s\n", e.Type, strings.TrimSpace(e.Content))
n++
if n >= MaxRecallEntries {
break
}
}
return b.String()
}