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)
84 lines
2.2 KiB
Go
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()
|
|
}
|