fix: improved memory tools (#495)

* fix: new prompt update tool

* fix: memory search tool

* fix: all review comments

* chore: remove dead code
This commit is contained in:
Dani Akash
2026-03-19 19:01:25 +05:30
committed by GitHub
parent d965698905
commit f4d4b73a24
6 changed files with 127 additions and 38 deletions

View File

@@ -415,8 +415,10 @@ You remember things across sessions using two tiers:
**Core memory** (\`CORE.md\`) — permanent facts about the user that persist forever.
Use for: name, job, location, preferences, relationships, recurring projects, important dates.
- \`memory_read_core\` → read all permanent facts
- \`memory_save_core\`save permanent facts
**IMPORTANT**: \`memory_save_core\` overwrites the entire file. Always call \`memory_read_core\` first, merge new facts into existing content, then save the full result.
- \`memory_update_core\`add or remove facts from core memory
Pass \`additions\` (array of new facts) and/or \`removals\` (array of facts to remove by substring match).
This tool handles merging internally — you never need to rewrite the full file.
Do NOT use \`memory_save_core\` — it is deprecated and risks overwriting all existing memories.
**Daily memory** — short-lived notes stored in daily files (\`YYYY-MM-DD.md\`). Auto-expire after 30 days.
Use for: what the user worked on today, transient context, meeting notes, draft ideas, things to follow up on.

View File

@@ -1,8 +1,8 @@
import type { ToolSet } from 'ai'
import { createReadCoreTool } from './read-core'
import { createSoulReadTool } from './read-soul'
import { createSaveCoreTool } from './save-core'
import { createMemorySearchTool } from './search'
import { createUpdateCoreTool } from './update-core'
import { createSoulUpdateTool } from './update-soul'
import { createMemoryWriteTool } from './write'
@@ -11,7 +11,7 @@ export function buildMemoryToolSet(): ToolSet {
memory_search: createMemorySearchTool(),
memory_write: createMemoryWriteTool(),
memory_read_core: createReadCoreTool(),
memory_save_core: createSaveCoreTool(),
memory_update_core: createUpdateCoreTool(),
soul_read: createSoulReadTool(),
soul_update: createSoulUpdateTool(),
}

View File

@@ -1,22 +0,0 @@
import { tool } from 'ai'
import { z } from 'zod'
import { getCoreMemoryPath } from '../../lib/browseros-dir'
import { executeWithMetrics, toModelOutput } from '../filesystem/utils'
const TOOL_NAME = 'memory_save_core'
export function createSaveCoreTool() {
return tool({
description:
'Write or update core memories. IMPORTANT: This overwrites the entire core memory file. Always call memory_read_core first, merge your changes into the existing content, then save the full result.',
inputSchema: z.object({
content: z.string().describe('The full core memory content to save'),
}),
execute: (params) =>
executeWithMetrics(TOOL_NAME, async () => {
await Bun.write(getCoreMemoryPath(), params.content)
return { text: 'Core memories updated.' }
}),
toModelOutput,
})
}

View File

@@ -28,10 +28,20 @@ async function loadMemoryEntries(): Promise<MemoryEntry[]> {
for (const file of mdFiles) {
try {
const content = await readFile(join(memoryDir, file), 'utf-8')
// Section-level entries (## delimited blocks)
const sections = content.split(/^## /m).filter(Boolean)
for (const section of sections) {
entries.push({ source: file, content: `## ${section}`.trim() })
}
// Line-level entries (individual non-empty lines)
for (const line of content.split('\n')) {
const trimmed = line.trim()
if (trimmed && !trimmed.startsWith('#')) {
entries.push({ source: file, content: trimmed })
}
}
} catch {
// skip unreadable files
}
@@ -84,11 +94,28 @@ export function createMemorySearchTool() {
}
}
const sorted = [...bestScores.entries()]
.sort((a, b) => a[1].score - b[1].score)
.slice(0, 10)
const sorted = [...bestScores.entries()].sort(
(a, b) => a[1].score - b[1].score,
)
const formatted = sorted
// Deduplicate: skip line-level hits already covered by a
// higher-ranked section from the same file.
const seenSections: Array<{ source: string; content: string }> = []
const deduped: typeof sorted = []
for (const item of sorted) {
const { source, content } = item[0]
const coveredBySection = seenSections.some(
(s) => s.source === source && s.content.includes(content),
)
if (coveredBySection) continue
if (content.startsWith('## ')) {
seenSections.push({ source, content })
}
deduped.push(item)
}
const formatted = deduped
.slice(0, 10)
.map(([entry, { score }]) => {
const relevance = (1 - score).toFixed(2)
return `[${entry.source}] (relevance: ${relevance})\n${entry.content}`

View File

@@ -0,0 +1,74 @@
import { tool } from 'ai'
import { z } from 'zod'
import { getCoreMemoryPath } from '../../lib/browseros-dir'
import { executeWithMetrics, toModelOutput } from '../filesystem/utils'
const TOOL_NAME = 'memory_update_core'
export function createUpdateCoreTool() {
return tool({
description:
'Add or remove facts from core memory. Handles merging internally — you never need to rewrite the full file. Pass additions to store new facts and/or removals to delete facts by substring match.',
inputSchema: z.object({
additions: z
.array(z.string())
.optional()
.describe('New facts to add to core memory. Each string is one fact.'),
removals: z
.array(z.string())
.optional()
.describe(
'Facts to remove from core memory. Each string is matched as a case-insensitive substring against existing lines.',
),
}),
execute: (params) =>
executeWithMetrics(TOOL_NAME, async () => {
const corePath = getCoreMemoryPath()
const file = Bun.file(corePath)
let existing = ''
if (await file.exists()) {
existing = await file.text()
}
let lines = existing.split('\n')
// Remove matching entries
let removedCount = 0
if (params.removals?.length) {
for (const removal of params.removals) {
const lower = removal.toLowerCase()
const before = lines.length
lines = lines.filter((line) => !line.toLowerCase().includes(lower))
removedCount += before - lines.length
}
}
// Append new facts (skip duplicates)
let addedCount = 0
if (params.additions?.length) {
if (lines.length > 0 && lines[lines.length - 1].trim() !== '') {
lines.push('')
}
for (const fact of params.additions) {
const factLower = fact.toLowerCase()
const alreadyExists = lines.some((l) =>
l.toLowerCase().includes(factLower),
)
if (!alreadyExists) {
lines.push(`- ${fact}`)
addedCount++
}
}
}
const result = `${lines.join('\n').trim()}\n`
await Bun.write(corePath, result)
return {
text: `Core memory updated. ${addedCount} fact(s) added, ${removedCount} line(s) removed.`,
}
}),
toModelOutput,
})
}

View File

@@ -289,7 +289,7 @@ describe('mode-aware framing', () => {
// The agent shouldn't even see memory tool instructions.
const prompt = buildChatMode()
expect(prompt).not.toContain('<memory_and_identity>')
expect(prompt).not.toContain('memory_save_core')
expect(prompt).not.toContain('memory_update_core')
expect(prompt).not.toContain('soul_update')
})
@@ -687,17 +687,25 @@ describe('memory and identity', () => {
const prompt = buildRegular()
expect(prompt).toContain('memory_search')
expect(prompt).toContain('memory_write')
expect(prompt).toContain('memory_save_core')
expect(prompt).toContain('memory_update_core')
expect(prompt).toContain('memory_read_core')
})
it('includes critical overwrite warning for memory_save_core', () => {
// Why: memory_save_core overwrites the entire file. Without the
// "read first, merge, then save" instruction, the agent will
// silently destroy existing memories when saving new ones.
it('discourages use of deprecated memory_save_core', () => {
// Why: memory_save_core overwrites the entire file and risks data loss.
// The prompt must steer the agent to memory_update_core instead.
const prompt = buildRegular()
expect(prompt).toContain('memory_save_core` overwrites the entire file')
expect(prompt).toContain('Always call `memory_read_core` first')
expect(prompt).toContain('Do NOT use `memory_save_core`')
expect(prompt).toContain('deprecated')
})
it('explains memory_update_core merge-based API', () => {
// Why: The agent must understand that memory_update_core handles merging
// internally via additions/removals — it should never rewrite the full file.
const prompt = buildRegular()
expect(prompt).toContain('additions')
expect(prompt).toContain('removals')
expect(prompt).toContain('handles merging internally')
})
it('explains two-tier memory model with core and daily distinction', () => {