mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 05:37:29 +08:00
200 lines
5.2 KiB
Go
200 lines
5.2 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",
|
|
},
|
|
"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, _ := args["content"].(string)
|
|
if content == "" {
|
|
return "error: content is required", nil
|
|
}
|
|
|
|
kind, _ := args["kind"].(string)
|
|
kind = strings.ToLower(strings.TrimSpace(kind))
|
|
if kind == "" {
|
|
kind = "daily"
|
|
}
|
|
|
|
importance, _ := args["importance"].(string)
|
|
importance = normalizeImportance(importance)
|
|
|
|
source, _ := args["source"].(string)
|
|
source = strings.TrimSpace(source)
|
|
if source == "" {
|
|
source = "user"
|
|
}
|
|
|
|
tags := parseTags(args["tags"])
|
|
|
|
appendMode := true
|
|
if v, ok := args["append"].(bool); 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(t.workspace, "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", path), nil
|
|
case "daily", "log", "today":
|
|
memDir := filepath.Join(t.workspace, "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", path), 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, ok := raw.([]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(items))
|
|
seen := map[string]struct{}{}
|
|
for _, it := range items {
|
|
s, _ := it.(string)
|
|
s = strings.ToLower(strings.TrimSpace(s))
|
|
if s == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[s]; exists {
|
|
continue
|
|
}
|
|
seen[s] = struct{}{}
|
|
out = append(out, s)
|
|
}
|
|
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
|
|
}
|