agent/pkg/agentic/brain_seed_memory.go
Virgil 547a481d7b feat(brain): seed MEMORY.md files only
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 03:51:47 +00:00

406 lines
10 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
iofs "io/fs"
"sort"
core "dappco.re/go/core"
)
const brainSeedMemoryDefaultAgent = "virgil"
const brainSeedMemoryDefaultPath = "~/.claude/projects/*/memory/"
type BrainSeedMemoryInput struct {
WorkspaceID int
AgentID string
Path string
DryRun bool
}
type BrainSeedMemoryOutput struct {
Success bool `json:"success"`
WorkspaceID int `json:"workspace_id,omitempty"`
AgentID string `json:"agent_id,omitempty"`
Path string `json:"path,omitempty"`
Files int `json:"files,omitempty"`
Imported int `json:"imported,omitempty"`
Skipped int `json:"skipped,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}
type brainSeedMemorySection struct {
Heading string
Content string
}
// result := c.Command("brain/seed-memory").Run(ctx, core.NewOptions(
//
// core.Option{Key: "workspace", Value: "1"},
// core.Option{Key: "path", Value: "/Users/snider/.claude/projects/*/memory/"},
//
// ))
func (s *PrepSubsystem) cmdBrainSeedMemory(options core.Options) core.Result {
return s.cmdBrainSeedMemoryLike(options, "brain seed-memory", "agentic.cmdBrainSeedMemory")
}
// result := c.Command("brain/ingest").Run(ctx, core.NewOptions(
//
// core.Option{Key: "workspace", Value: "1"},
// core.Option{Key: "path", Value: "/Users/snider/.claude/projects/*/memory/"},
//
// ))
func (s *PrepSubsystem) cmdBrainIngest(options core.Options) core.Result {
return s.cmdBrainSeedMemoryLikeMode(options, "brain ingest", "agentic.cmdBrainIngest", false)
}
func (s *PrepSubsystem) cmdBrainSeedMemoryLike(options core.Options, commandName string, errorLabel string) core.Result {
return s.cmdBrainSeedMemoryLikeMode(options, commandName, errorLabel, true)
}
func (s *PrepSubsystem) cmdBrainSeedMemoryLikeMode(options core.Options, commandName string, errorLabel string, memoryFilesOnly bool) core.Result {
input := BrainSeedMemoryInput{
WorkspaceID: parseIntString(optionStringValue(options, "workspace", "workspace_id", "workspace-id", "_arg")),
AgentID: optionStringValue(options, "agent", "agent_id", "agent-id"),
Path: optionStringValue(options, "path"),
DryRun: optionBoolValue(options, "dry-run"),
}
if input.WorkspaceID == 0 {
core.Print(nil, "usage: core-agent %s --workspace=1 [--agent=virgil] [--path=~/.claude/projects/*/memory/] [--dry-run]", commandName)
return core.Result{Value: core.E(errorLabel, "workspace is required", nil), OK: false}
}
if input.AgentID == "" {
input.AgentID = brainSeedMemoryDefaultAgent
}
if input.Path == "" {
input.Path = brainSeedMemoryDefaultPath
}
result := s.brainSeedMemory(s.commandContext(), input, memoryFilesOnly)
if !result.OK {
err := commandResultError(errorLabel, result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(BrainSeedMemoryOutput)
if !ok {
err := core.E(errorLabel, "invalid brain seed memory output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
if output.Files == 0 {
core.Print(nil, "No markdown memory files found in: %s", output.Path)
return core.Result{Value: output, OK: true}
}
prefix := ""
if output.DryRun {
prefix = "[DRY RUN] "
}
core.Print(nil, "%sImported %d memories, skipped %d.", prefix, output.Imported, output.Skipped)
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) brainSeedMemory(ctx context.Context, input BrainSeedMemoryInput, memoryFilesOnly bool) core.Result {
if s.brainKey == "" {
return core.Result{Value: core.E("agentic.brainSeedMemory", "no brain API key configured", nil), OK: false}
}
scanPath := brainSeedMemoryScanPath(input.Path)
files := brainSeedMemoryFiles(scanPath, memoryFilesOnly)
output := BrainSeedMemoryOutput{
Success: true,
WorkspaceID: input.WorkspaceID,
AgentID: input.AgentID,
Path: scanPath,
Files: len(files),
DryRun: input.DryRun,
}
if len(files) == 0 {
return core.Result{Value: output, OK: true}
}
for _, path := range files {
readResult := fs.Read(path)
if !readResult.OK {
output.Skipped++
continue
}
sections := brainSeedMemorySections(readResult.Value.(string))
if len(sections) == 0 {
output.Skipped++
core.Print(nil, " Skipped %s (no sections found)", core.PathBase(path))
continue
}
project := brainSeedMemoryProject(path)
filename := core.TrimSuffix(core.PathBase(path), ".md")
for _, section := range sections {
memoryType := brainSeedMemoryType(section.Heading, section.Content)
if input.DryRun {
core.Print(nil, " [DRY RUN] %s :: %s (%s) — %d chars", core.PathBase(path), section.Heading, memoryType, core.RuneCount(section.Content))
output.Imported++
continue
}
body := map[string]any{
"workspace_id": input.WorkspaceID,
"agent_id": input.AgentID,
"type": memoryType,
"content": core.Concat(section.Heading, "\n\n", section.Content),
"tags": brainSeedMemoryTags(filename),
"project": project,
"confidence": 0.7,
}
if result := HTTPPost(ctx, core.Concat(s.brainURL, "/v1/brain/remember"), core.JSONMarshalString(body), s.brainKey, "Bearer"); !result.OK {
output.Skipped++
core.Print(nil, " Failed to import %s :: %s", core.PathBase(path), section.Heading)
continue
}
output.Imported++
}
}
return core.Result{Value: output, OK: true}
}
func brainSeedMemoryScanPath(path string) string {
trimmed := brainSeedMemoryExpandHome(core.Trim(path))
if trimmed == "" {
return brainSeedMemoryExpandHome(brainSeedMemoryDefaultPath)
}
if fs.IsFile(trimmed) {
return trimmed
}
return trimmed
}
func brainSeedMemoryExpandHome(path string) string {
if core.HasPrefix(path, "~/") {
return core.Concat(HomeDir(), core.TrimPrefix(path, "~"))
}
return path
}
func brainSeedMemoryFiles(scanPath string, memoryFilesOnly bool) []string {
if scanPath == "" {
return nil
}
var files []string
seen := map[string]struct{}{}
add := func(path string) {
if path == "" {
return
}
if _, ok := seen[path]; ok {
return
}
seen[path] = struct{}{}
files = append(files, path)
}
var walk func(string)
walk = func(dir string) {
if fs.IsFile(dir) {
if brainSeedMemoryFile(dir, memoryFilesOnly) {
add(dir)
}
return
}
if !fs.IsDir(dir) {
return
}
r := fs.List(dir)
if !r.OK {
return
}
entries, ok := r.Value.([]iofs.DirEntry)
if !ok {
return
}
for _, entry := range entries {
next := core.JoinPath(dir, entry.Name())
if entry.IsDir() {
walk(next)
continue
}
if brainSeedMemoryFile(next, memoryFilesOnly) {
add(next)
}
}
}
if fs.IsFile(scanPath) {
if brainSeedMemoryFile(scanPath, memoryFilesOnly) {
add(scanPath)
}
sort.Strings(files)
return files
}
if brainSeedMemoryHasGlobMeta(scanPath) {
for _, path := range core.PathGlob(scanPath) {
if fs.IsFile(path) {
if brainSeedMemoryFile(path, memoryFilesOnly) {
add(path)
}
continue
}
walk(path)
}
} else {
walk(scanPath)
}
sort.Strings(files)
return files
}
func brainSeedMemoryHasGlobMeta(path string) bool {
return core.Contains(path, "*") || core.Contains(path, "?") || core.Contains(path, "[")
}
func brainSeedMemoryFile(path string, memoryFilesOnly bool) bool {
if memoryFilesOnly {
return core.PathBase(path) == "MEMORY.md"
}
return core.Lower(core.PathExt(path)) == ".md"
}
func brainSeedMemorySections(content string) []brainSeedMemorySection {
lines := core.Split(content, "\n")
var sections []brainSeedMemorySection
currentHeading := ""
var currentContent []string
flush := func() {
if currentHeading == "" || len(currentContent) == 0 {
return
}
sectionContent := core.Trim(core.Join("\n", currentContent...))
if sectionContent == "" {
return
}
sections = append(sections, brainSeedMemorySection{
Heading: currentHeading,
Content: sectionContent,
})
}
for _, line := range lines {
if heading, ok := brainSeedMemoryHeading(line); ok {
flush()
currentHeading = heading
currentContent = currentContent[:0]
continue
}
if currentHeading == "" {
continue
}
currentContent = append(currentContent, line)
}
flush()
return sections
}
func brainSeedMemoryHeading(line string) (string, bool) {
trimmed := core.Trim(line)
if trimmed == "" || !core.HasPrefix(trimmed, "#") {
return "", false
}
hashes := 0
for _, r := range trimmed {
if r != '#' {
break
}
hashes++
}
if hashes < 1 || hashes > 3 || len(trimmed) <= hashes || trimmed[hashes] != ' ' {
return "", false
}
heading := core.Trim(trimmed[hashes:])
if heading == "" {
return "", false
}
return heading, true
}
func brainSeedMemoryType(heading, content string) string {
lower := core.Lower(core.Concat(heading, " ", content))
for _, candidate := range []struct {
memoryType string
keywords []string
}{
{memoryType: "architecture", keywords: []string{"architecture", "stack", "infrastructure", "layer", "service mesh"}},
{memoryType: "convention", keywords: []string{"convention", "standard", "naming", "pattern", "rule", "coding"}},
{memoryType: "decision", keywords: []string{"decision", "chose", "strategy", "approach", "domain"}},
{memoryType: "bug", keywords: []string{"bug", "fix", "broken", "error", "issue", "lesson"}},
{memoryType: "plan", keywords: []string{"plan", "todo", "roadmap", "milestone", "phase"}},
{memoryType: "research", keywords: []string{"research", "finding", "discovery", "analysis", "rfc"}},
} {
for _, keyword := range candidate.keywords {
if core.Contains(lower, keyword) {
return candidate.memoryType
}
}
}
return "observation"
}
func brainSeedMemoryTags(filename string) []string {
if filename == "" {
return []string{"memory-import"}
}
tags := []string{}
if core.Lower(filename) != "memory" {
tag := core.Replace(core.Replace(filename, "-", " "), "_", " ")
if tag != "" {
tags = append(tags, tag)
}
}
tags = append(tags, "memory-import")
return tags
}
func brainSeedMemoryProject(path string) string {
normalised := core.Replace(path, "\\", "/")
segments := core.Split(normalised, "/")
for i := 1; i < len(segments); i++ {
if segments[i] != "memory" {
continue
}
projectSegment := segments[i-1]
if projectSegment == "" {
continue
}
chunks := core.Split(projectSegment, "-")
for j := len(chunks) - 1; j >= 0; j-- {
if chunks[j] != "" {
return chunks[j]
}
}
}
return ""
}