Files
clawgo/pkg/tools/memory_write.go

194 lines
5.4 KiB
Go

package tools
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
type MemoryWriteTool struct {
workspace string
}
func NewMemoryWriteTool(workspace string) *MemoryWriteTool {
return &MemoryWriteTool{workspace: workspace}
}
func (t *MemoryWriteTool) Name() string {
return "memory_write"
}
func (t *MemoryWriteTool) Description() string {
return "Write memory entries to long-term MEMORY.md or daily memory/YYYY-MM-DD.md. Use longterm for durable preferences/decisions, daily for raw logs."
}
func (t *MemoryWriteTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"content": map[string]interface{}{
"type": "string",
"description": "Memory text to write",
},
"namespace": map[string]interface{}{
"type": "string",
"description": "Optional memory namespace. Use main for workspace memory, or subagent id for isolated memory.",
"default": "main",
},
"kind": map[string]interface{}{
"type": "string",
"description": "Target memory kind: longterm or daily",
"default": "daily",
},
"importance": map[string]interface{}{
"type": "string",
"description": "low|medium|high. high is recommended for longterm",
"default": "medium",
},
"source": map[string]interface{}{
"type": "string",
"description": "Source/context for this memory, e.g. user, system, tool",
"default": "user",
},
"tags": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "string"},
"description": "Optional tags for filtering/search, e.g. preference,todo,decision",
},
"append": map[string]interface{}{
"type": "boolean",
"description": "Append mode (default true)",
"default": true,
},
},
"required": []string{"content"},
}
}
func (t *MemoryWriteTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) {
content := MapRawStringArg(args, "content")
if content == "" {
return "error: content is required", nil
}
namespace := parseMemoryNamespaceArg(args)
baseDir := memoryNamespaceBaseDir(t.workspace, namespace)
kind := strings.ToLower(MapStringArg(args, "kind"))
if kind == "" {
kind = "daily"
}
importance := normalizeImportance(MapStringArg(args, "importance"))
source := MapStringArg(args, "source")
if source == "" {
source = "user"
}
tags := parseTags(args["tags"])
appendMode := true
if v, ok := MapBoolArg(args, "append"); ok {
appendMode = v
}
formatted := formatMemoryLine(content, importance, source, tags)
if (kind == "longterm" || kind == "memory" || kind == "permanent") && !allowLongTermWrite(importance, tags) {
kind = "daily"
formatted = formatMemoryLine(content, importance, source, append(tags, "downgraded:longterm_gate"))
}
switch kind {
case "longterm", "memory", "permanent":
path := filepath.Join(baseDir, "MEMORY.md")
if appendMode {
return t.appendWithTimestamp(path, formatted)
}
if err := os.WriteFile(path, []byte(formatted+"\n"), 0644); err != nil {
return "", err
}
return fmt.Sprintf("Wrote long-term memory: %s (namespace=%s)", path, namespace), nil
case "daily", "log", "today":
memDir := filepath.Join(baseDir, "memory")
if err := os.MkdirAll(memDir, 0755); err != nil {
return "", err
}
path := filepath.Join(memDir, time.Now().Format("2006-01-02")+".md")
if appendMode {
return t.appendWithTimestamp(path, formatted)
}
if err := os.WriteFile(path, []byte(formatted+"\n"), 0644); err != nil {
return "", err
}
return fmt.Sprintf("Wrote daily memory: %s (namespace=%s)", path, namespace), nil
default:
return "", fmt.Errorf("invalid kind '%s', expected longterm or daily", kind)
}
}
func (t *MemoryWriteTool) appendWithTimestamp(path, content string) (string, error) {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return "", err
}
line := fmt.Sprintf("- [%s] %s\n", time.Now().Format("15:04"), content)
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return "", err
}
defer f.Close()
if _, err := f.WriteString(line); err != nil {
return "", err
}
return fmt.Sprintf("Appended memory to %s", path), nil
}
func normalizeImportance(v string) string {
s := strings.ToLower(strings.TrimSpace(v))
switch s {
case "high", "medium", "low":
return s
default:
return "medium"
}
}
func parseTags(raw interface{}) []string {
items := MapStringListArg(map[string]interface{}{"tags": raw}, "tags")
if len(items) == 0 {
return nil
}
out := make([]string, 0, len(items))
for _, item := range items {
out = append(out, strings.ToLower(strings.TrimSpace(item)))
}
return out
}
func formatMemoryLine(content, importance, source string, tags []string) string {
var meta []string
meta = append(meta, "importance="+importance)
meta = append(meta, "source="+source)
if len(tags) > 0 {
meta = append(meta, "tags="+strings.Join(tags, ","))
}
return fmt.Sprintf("[%s] %s", strings.Join(meta, " | "), content)
}
func allowLongTermWrite(importance string, tags []string) bool {
if strings.ToLower(strings.TrimSpace(importance)) == "high" {
return true
}
for _, t := range tags {
s := strings.ToLower(strings.TrimSpace(t))
switch s {
case "preference", "decision", "rule", "policy", "identity":
return true
}
}
return false
}