返回 DeepSeek-Reasonix
remember.go
根目录 / internal / memory / remember.go
1 package memory
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 "reasonix/internal/tool"
10 )
11
12 // rememberTool lets the model persist a durable fact to the auto-memory store.
13 // It is stateful (bound to one project's Store), so boot constructs it and adds
14 // it to the registry — the same pattern as the task tool — rather than
15 // self-registering as a stateless built-in.
16 type rememberTool struct{ store Store }
17
18 type rememberRequest struct {
19 ID string `json:"id"`
20 ExpectedRevision int `json:"expected_revision"`
21 Name string `json:"name"`
22 Title string `json:"title"`
23 Description string `json:"description"`
24 Type string `json:"type"`
25 Scope string `json:"scope"`
26 Body string `json:"body"`
27 }
28
29 // NewRememberTool returns the `remember` tool bound to store. A zero/disabled
30 // store yields a tool that reports the store is unavailable rather than silently
31 // dropping saves.
32 func NewRememberTool(store Store) tool.Tool { return rememberTool{store: store} }
33
34 func (rememberTool) Name() string { return "remember" }
35
36 func (rememberTool) Description() string {
37 return "Save a durable background fact so it survives across sessions. " +
38 "Use for things worth remembering long-term: who the user is and their preferences (type \"user\"); " +
39 "guidance on how to work, including the why (type \"feedback\"); ongoing goals or constraints not " +
40 "derivable from the code (type \"project\"); or pointers to external resources (type \"reference\"). " +
41 "For feedback/project, structure the body with a \"**Why:**\" line and a \"**How to apply:**\" line so the fact is actionable later; " +
42 "link related memories inline with [[their-name]]. " +
43 "Do NOT save what the repo already records (code structure, git history) or facts that only matter to the current conversation; " +
44 "if asked to remember one of those, save instead the non-obvious point behind it. " +
45 "Choose scope \"project\" for the current workspace (the safe default) or \"global\" only when the fact should affect every project. " +
46 "Standing rules that must always be followed belong in project or global REASONIX.md/AGENTS.md instructions, not background memory. " +
47 "Before saving, check the loaded memory index for an entry that already covers this — reuse that name to update it rather than create a near-duplicate, and use `forget` to drop one that is now wrong. " +
48 "The saved index loads into context at the start of each session."
49 }
50
51 func (rememberTool) Schema() json.RawMessage {
52 return json.RawMessage(`{
53 "type": "object",
54 "properties": {
55 "id": {"type": "string", "description": "Stable memory id for an update. Prefer this over name when supplied by memory search/read."},
56 "expected_revision": {"type": "integer", "minimum": 1, "description": "Revision returned by memory search/read. When set with id, the update fails instead of overwriting a newer change."},
57 "name": {"type": "string", "description": "Stable project/<name>.md or global/<name>.md reference returned by memory search/read/list, or a short kebab-case slug for a new fact. Reusing a reference updates that exact memory. Omit to derive a new slug from the description."},
58 "title": {"type": "string", "description": "Short human-readable label shown in the memory index, e.g. \"Prefers tabs\". Omit to derive one from the name."},
59 "description": {"type": "string", "description": "One-line hook shown in the index — the phrase a future session reads to decide whether to open this memory. Make it specific."},
60 "type": {"type": "string", "enum": ["user", "feedback", "project", "reference"], "description": "Category of the fact."},
61 "scope": {"type": "string", "enum": ["project", "global"], "description": "Where the fact applies. For a new fact, omit for the safe default, project. When updating an existing name, omit to preserve its current scope. Use global only when it should affect every workspace."},
62 "body": {"type": "string", "description": "The fact itself (Markdown). For feedback/project, include a \"**Why:**\" line and a \"**How to apply:**\" line; link related memories with [[their-name]]."}
63 },
64 "required": ["description", "body"]
65 }`)
66 }
67
68 func (t rememberTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
69 in, err := parseRememberRequest(args)
70 if err != nil {
71 return "", err
72 }
73 if in.Description == "" || in.Body == "" {
74 return "", fmt.Errorf("description and body are required")
75 }
76 scope := strings.ToLower(strings.TrimSpace(in.Scope))
77 if scope != "" && scope != string(FactScopeProject) && scope != string(FactScopeGlobal) {
78 return "", fmt.Errorf("scope must be one of project, global")
79 }
80 factScope := FactScope(scope)
81 name := rememberRequestName(in)
82 autoCreate := ClaimAutoMemoryWriteFromContext(ctx, args)
83 result, err := t.store.SaveWithOptions(Memory{
84 ID: in.ID,
85 Name: name,
86 Title: in.Title,
87 Description: in.Description,
88 Type: NormalizeType(in.Type),
89 Scope: factScope,
90 Body: in.Body,
91 }, SaveOptions{
92 ExpectedRevision: in.ExpectedRevision,
93 RequireExpectedRevision: in.ExpectedRevision > 0,
94 RequireCreate: autoCreate,
95 })
96 if err != nil {
97 return "", err
98 }
99 path := result.Path
100 if saved, ok := loadMemory(path); ok && saved.Scope != "" {
101 factScope = NormalizeFactScope(string(saved.Scope))
102 } else {
103 factScope = t.store.scopeForPath(path)
104 }
105 if q, ok := QueueFromContext(ctx); ok {
106 q.QueueMemory("Saved memory \"" + result.Memory.Name + "\" (" + string(factScope) + "): " + oneLine(result.Memory.Description) + "\n" + strings.TrimSpace(result.Memory.Body))
107 }
108 return fmt.Sprintf("Saved memory id=%s revision=%d (%s background) as %s (it applies now and its derived index loads automatically in future sessions).", result.Memory.ID, result.Memory.Revision, factScope, providerMemoryReference(result.Memory)), nil
109 }
110
111 func (rememberTool) ReadOnly() bool { return false }
112
113 func parseRememberRequest(args json.RawMessage) (rememberRequest, error) {
114 var in rememberRequest
115 if err := json.Unmarshal(args, &in); err != nil {
116 return rememberRequest{}, fmt.Errorf("invalid arguments: %w", err)
117 }
118 return in, nil
119 }
120
121 func rememberRequestName(in rememberRequest) string {
122 if name := strings.TrimSpace(in.Name); name != "" {
123 return name
124 }
125 name := ""
126 if in.ID == "" {
127 name = in.Title
128 }
129 if name == "" && in.ID == "" {
130 name = in.Description
131 }
132 return slug(name)
133 }
134
134 lines GO