Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,49 @@ If `userId` is omitted, memory is stored in the shared default scope. That is us
- `memory.learn: false`: disables learning for a single call.
- `memory.recall: false`: disables recall for a single call.

## Data Migration

Use `agent.memory.migrate()` when an app already has memory-like data and wants to seed the configured persistence layer. The SDK does not fetch or map source data; pass normalized memories or events from your own import code.

```ts
await agent.memory.migrate({
userId: "user_123",
data: [
{
type: "preference",
content: "User prefers concise weekly reports.",
confidence: 0.9,
importance: 0.8
}
]
})
```

Migration validates the input, resolves the target scope, writes through the configured memory store, links source events when provided, creates a synthetic source event for bare memory rows, and stores embeddings so imported memory can be recalled immediately. Use `mode: "skipExisting"` to preserve an existing memory with the same ID or canonical key.

```ts
await agent.memory.migrate({
userId: "user_123",
mode: "skipExisting",
events: [
{
id: "evt_legacy_1",
role: "user",
content: "Legacy note: the customer portal was renamed to Atlas."
}
],
memories: [
{
id: "mem_legacy_1",
type: "fact",
content: "The customer portal was renamed to Atlas.",
canonicalKey: "fact:customer-portal-renamed",
sourceEventIds: ["evt_legacy_1"]
}
]
})
```

## Storage

The public `agent-memory-sdk` package defaults to local JSON persistence:
Expand Down
20 changes: 20 additions & 0 deletions packages/agent-memory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,23 @@ console.log(result.text)
First-party helpers include `openai()`, `anthropic()`, `gemini()`, and `xai()`. Use `openAICompatible()` for custom chat-completions endpoints.

If `userId` is omitted, memory is stored in the shared default scope. Local memory defaults to `.memory/memory.json`; use `sqliteMemory()` for `.memory/memory.sqlite` or `postgresMemory()` for Postgres with automatic pgvector migrations.

## Data Migration

Seed existing app data by passing normalized memories or events to the configured memory store:

```ts
await agent.memory.migrate({
userId: "user_123",
data: [
{
type: "preference",
content: "User prefers concise weekly reports.",
confidence: 0.9,
importance: 0.8
}
]
})
```

The SDK only validates and stores mapped input. Your app owns where the data comes from and how it is mapped. Use `events` and `sourceEventIds` when you want imported memories linked to imported history, and `mode: "skipExisting"` to leave matching memories unchanged.
115 changes: 115 additions & 0 deletions packages/agent-memory/test/agent.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,121 @@ test("generate without userId writes to the default scope", async () => {
assert.match(memories[0]?.content ?? "", /prefers? short answers/i)
})

test("memory migration imports caller-mapped data and makes it recallable", async () => {
const seenRequests = []
const agent = createAgent({
model: customModel({
id: "test-model",
generate: async (request) => {
seenRequests.push(request)
return { text: "ok" }
}
}),
memory: createMemoryStore()
})

const report = await agent.memory.migrate({
userId: "migrated_user",
data: [
{
type: "preference",
content: "User prefers dashboard summaries as bullet lists.",
confidence: 0.92,
importance: 0.81
}
]
})

assert.equal(report.memories.created, 1)
assert.equal(report.memories.updated, 0)
assert.equal(report.memories.skipped, 0)
assert.equal(report.memories.failed, 0)
assert.equal(report.events.imported, 1)
assert.deepEqual(report.failures, [])

const memories = await agent.memory.list({ userId: "migrated_user" })
assert.equal(memories.length, 1)
assert.equal(memories[0]?.scopeKey, "user:migrated_user")
assert.equal(memories[0]?.type, "preference")
assert.match(memories[0]?.canonicalKey ?? "", /^preference:/)

await agent.generate({
userId: "migrated_user",
messages: [
{ role: "user", content: "How should I format the dashboard summary?" }
],
debug: true
})

const recalledRequest = seenRequests.at(-1)
assert.equal(recalledRequest.messages[0]?.role, "system")
assert.match(recalledRequest.messages[0]?.content ?? "", /dashboard summaries as bullet lists/i)
})

test("memory migration imports mapped events and can skip existing memories", async () => {
const agent = createAgent({
model: customModel({
id: "test-model",
generate: async () => ({ text: "ok" })
}),
memory: createMemoryStore()
})

const first = await agent.memory.migrate({
userId: "legacy_user",
threadId: "legacy_thread",
operationId: "legacy_import",
events: [
{
id: "evt_legacy_1",
role: "user",
content: "Legacy note: the customer portal was renamed to Atlas.",
metadata: { source: "legacy-export" },
createdAt: "2026-01-01T00:00:00.000Z"
}
],
memories: [
{
id: "mem_legacy_1",
type: "fact",
content: "The customer portal was renamed to Atlas.",
canonicalKey: "fact:customer-portal-renamed",
sourceEventIds: ["evt_legacy_1"],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z"
}
]
})

assert.equal(first.events.imported, 1)
assert.equal(first.memories.created, 1)
assert.equal(first.memories.updated, 0)

const second = await agent.memory.migrate({
userId: "legacy_user",
mode: "skipExisting",
memories: [
{
type: "fact",
content: "The customer portal has a different migrated description.",
canonicalKey: "fact:customer-portal-renamed"
}
]
})

assert.equal(second.memories.created, 0)
assert.equal(second.memories.updated, 0)
assert.equal(second.memories.skipped, 1)

const memories = await agent.memory.list({ userId: "legacy_user" })
const exported = await agent.memory.export({ userId: "legacy_user" })

assert.equal(memories.length, 1)
assert.equal(memories[0]?.id, "mem_legacy_1")
assert.equal(memories[0]?.content, "The customer portal was renamed to Atlas.")
assert.equal(exported.events.some((event) => event.id === "evt_legacy_1"), true)
})

test("stream returns text and commits memory after consumption", async () => {
const agent = createAgent({
model: customModel({
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto"
import { createDefaultCompiler, canonicalKeyFor, validateMemoryPatch } from "./compiler.js"
import { createMemoryStore, makeEmbedding, makeEvent } from "./memory-store.js"
import { migrateMemory } from "./migration.js"
import { retrieveMemoryContext } from "./retrieval.js"
import { resolveScope, targetScopeKeys } from "./scopes.js"
import type {
Expand All @@ -10,6 +11,7 @@ import type {
CompilerProvider,
GenerateInput,
GenerateResult,
MemoryMigrationInput,
MemoryPatch,
MemoryRecord,
MemoryScopeInput,
Expand Down Expand Up @@ -221,6 +223,11 @@ export function createAgent(config: AgentConfig): Agent {
})
},

async migrate(input: MemoryMigrationInput) {
ensureStore(store)
return migrateMemory({ store, migration: input })
},

async delete(memoryId: string) {
ensureStore(store)
return store.deleteMemory(memoryId)
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { createAgent } from "./agent.js"
export { createDefaultCompiler, canonicalKeyFor, validateMemoryPatch } from "./compiler.js"
export { createMemoryStore, makeEmbedding, makeEvent, tokenize } from "./memory-store.js"
export { migrateMemory } from "./migration.js"
export { customModel } from "./providers.js"
export { retrieveMemoryContext } from "./retrieval.js"
export { resolveScope, targetScopeKeys } from "./scopes.js"
Expand Down
Loading
Loading