@ppagent/memory 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/dist/index.d.ts +159 -117
  2. package/dist/index.js +326 -157
  3. package/llms.txt +845 -0
  4. package/package.json +10 -8
package/llms.txt ADDED
@@ -0,0 +1,845 @@
1
+ # @ppagent/memory
2
+
3
+ > `@ppagent/memory` is an independent TypeScript/Node.js long-term memory package for AI agents. It stores conversation memory in LanceDB, extracts and searches entity relationships with Grafeo, supports user/chat/session scoped facts, compresses chat history into multi-resolution topics, and ingests Markdown documents as a searchable knowledge base.
4
+
5
+ This file is written for AI coding agents and assistants. Use it as the primary package guide when generating code that depends on `@ppagent/memory`.
6
+
7
+ ## Package Identity
8
+
9
+ - Package name: `@ppagent/memory`
10
+ - Module format: ESM
11
+ - Main entry: `dist/index.js`
12
+ - Type entry: `dist/index.d.ts`
13
+ - Source entry during local workspace development: `src/index.ts`
14
+ - Runtime target: Node.js with global `fetch`
15
+ - Storage engines:
16
+ - LanceDB for messages, topics, facts, sessions, documents, and chunks
17
+ - Grafeo for entity/relation graph storage and graph search
18
+ - LLM APIs: OpenAI-compatible `/chat/completions`
19
+ - Embedding APIs: OpenAI-compatible `/embeddings`
20
+
21
+ ## When To Use This Package
22
+
23
+ Use `@ppagent/memory` when an AI agent needs:
24
+
25
+ - Long-term conversation memory across sessions.
26
+ - Recent message storage plus compressed historical context windows.
27
+ - User-level or chat-level facts such as preferences, profile information, project decisions, or instructions that should persist.
28
+ - Hybrid semantic/full-text search over remembered topics and messages.
29
+ - Optional graph search for entity-relationship questions.
30
+ - Markdown knowledge-base ingestion with chunking, vector search, document-level coarse recall, and optional knowledge graph construction.
31
+ - Session management APIs for agent dashboards or memory administration pages.
32
+ - A standalone memory layer that does not depend on the larger PPAgent runtime.
33
+
34
+ Do not use it as a full agent framework by itself. It does not send chat messages, call tools for users, route platform webhooks, or run a frontend. It is a memory and retrieval module.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pnpm add @ppagent/memory
40
+ ```
41
+
42
+ ```bash
43
+ npm install @ppagent/memory
44
+ ```
45
+
46
+ ## Imports
47
+
48
+ ```ts
49
+ import {
50
+ MemoryManager,
51
+ LanceService,
52
+ DEFAULT_NODE_TYPES,
53
+ DEFAULT_RELATION_TYPES,
54
+ KIND_CONVERSATION,
55
+ KIND_KNOWLEDGE,
56
+ } from "@ppagent/memory";
57
+
58
+ import type {
59
+ MemoryConfig,
60
+ RawMessage,
61
+ SearchOptions,
62
+ SearchResult,
63
+ AddDocumentOptions,
64
+ KnowledgeSearchOptions,
65
+ KnowledgeSearchResult,
66
+ } from "@ppagent/memory";
67
+ ```
68
+
69
+ Most consumers should only instantiate `MemoryManager`. `LanceService` is exported for advanced storage tests or custom infrastructure work.
70
+
71
+ ## Minimal Example
72
+
73
+ ```ts
74
+ import { MemoryManager } from "@ppagent/memory";
75
+
76
+ const memory = new MemoryManager({
77
+ lancedbPath: "./data/memory/lance",
78
+ grafeoPath: "./data/memory/grafeo",
79
+
80
+ llmBaseUrl: process.env.LLM_BASE_URL ?? "https://api.openai.com/v1",
81
+ llmApiKey: process.env.LLM_API_KEY ?? "",
82
+ llmModel: process.env.LLM_MODEL ?? "gpt-4o-mini",
83
+
84
+ embeddingBaseUrl: process.env.EMBED_BASE_URL ?? process.env.LLM_BASE_URL ?? "https://api.openai.com/v1",
85
+ embeddingApiKey: process.env.EMBED_API_KEY ?? process.env.LLM_API_KEY ?? "",
86
+ embeddingModel: process.env.EMBED_MODEL ?? "text-embedding-3-small",
87
+ embeddingDimension: Number(process.env.EMBED_DIMENSION ?? 1536),
88
+ });
89
+
90
+ await memory.init();
91
+
92
+ await memory.updateChat(
93
+ [
94
+ { talkerId: "user", content: "我叫 Bob,正在做一个 Rust 项目。" },
95
+ { talkerId: "assistant", content: "好的,我会记住你在做 Rust 项目。" },
96
+ ],
97
+ {
98
+ userId: "user-bob",
99
+ chatId: "agent-dev",
100
+ sessionId: "session-001",
101
+ sessionTitle: "Bob 的开发对话",
102
+ }
103
+ );
104
+
105
+ const results = await memory.search({
106
+ query: "Bob 正在做什么项目?",
107
+ scope: "user",
108
+ scopeId: "user-bob",
109
+ mode: "auto",
110
+ });
111
+
112
+ console.log(results);
113
+
114
+ const answer = await memory.ask({
115
+ query: "Bob 正在做什么项目?",
116
+ scope: "user",
117
+ scopeId: "user-bob",
118
+ maxChars: 120,
119
+ });
120
+
121
+ console.log(answer);
122
+
123
+ memory.destroy();
124
+ ```
125
+
126
+ ## Complete Example
127
+
128
+ ```ts
129
+ import { MemoryManager } from "@ppagent/memory";
130
+ import type { Entity, Relation } from "@ppagent/memory";
131
+
132
+ const memory = new MemoryManager({
133
+ lancedbPath: "./data/memory/lance",
134
+ grafeoPath: "./data/memory/grafeo",
135
+
136
+ llmBaseUrl: process.env.LLM_BASE_URL ?? "https://api.openai.com/v1",
137
+ llmApiKey: process.env.LLM_API_KEY ?? "",
138
+ llmModel: process.env.LLM_MODEL ?? "gpt-4o-mini",
139
+
140
+ embeddingBaseUrl: process.env.EMBED_BASE_URL ?? "",
141
+ embeddingApiKey: process.env.EMBED_API_KEY ?? "",
142
+ embeddingModel: process.env.EMBED_MODEL ?? "text-embedding-3-small",
143
+ embeddingDimension: 1536,
144
+
145
+ httpTimeoutMs: 60_000,
146
+ httpMaxRetries: 2,
147
+ embeddingBatchSize: 20,
148
+ embeddingConcurrency: 2,
149
+
150
+ sessionTokenLimit: 16_386,
151
+ historyWindowTokenLimit: 10_240,
152
+ topicRatio: [1, 5, 20],
153
+ detailMaxTokens: 2048,
154
+ summaryMaxTokens: 512,
155
+ conciseMaxTokens: 128,
156
+ maxConcurrentCompressions: 3,
157
+ entitySimilarityThreshold: 0.92,
158
+ defaultSearchLimit: 10,
159
+ recallBoostMs: 3_600_000,
160
+
161
+ chunkStrategy: "markdown-heading",
162
+ chunkMaxTokens: 800,
163
+ chunkOverlap: 0,
164
+ knowledgeTopK: 8,
165
+ docCoarseTopK: 5,
166
+ buildGraphDefault: "auto",
167
+ chunkRedundantIds: true,
168
+ knowledgeGraphTriggerScore: 0.78,
169
+ knowledgeGraphEntityTopK: 10,
170
+ knowledgeGraphAnchorTopK: 3,
171
+ knowledgeGraphHopLimit: 10,
172
+ graphExtractConcurrency: 2,
173
+ graphBuildTimeoutMs: 120_000,
174
+ });
175
+
176
+ await memory.init();
177
+
178
+ const userId = "u-001";
179
+ const chatId = "agent-work";
180
+ const sessionId = "s-2026-06-12";
181
+
182
+ await memory.updateChat(
183
+ [
184
+ {
185
+ talkerId: "user",
186
+ content: "我叫 Alice,在 ChatMe 项目中负责后端架构。",
187
+ metadata: { source: "chat" },
188
+ },
189
+ {
190
+ talkerId: "assistant",
191
+ content: "明白,我会记住你负责 ChatMe 后端架构。",
192
+ },
193
+ ],
194
+ {
195
+ userId,
196
+ chatId,
197
+ sessionId,
198
+ sessionTitle: "ChatMe 架构讨论",
199
+ sessionMetadata: { product: "ChatMe" },
200
+ }
201
+ );
202
+
203
+ await memory.updateFacts(
204
+ "Alice 负责 ChatMe 项目的后端架构",
205
+ "user",
206
+ userId,
207
+ chatId,
208
+ sessionId
209
+ );
210
+
211
+ const entities: Entity[] = [
212
+ { name: "Alice", type: "Person", meta: { role: "backend architect", userId, chatId, sessionId } },
213
+ { name: "ChatMe", type: "Project", meta: { userId, chatId, sessionId } },
214
+ { name: "TypeScript", type: "Technology", meta: { userId, chatId, sessionId } },
215
+ ];
216
+
217
+ const relations: Relation[] = [
218
+ { from: "Alice", to: "ChatMe", type: "works_on", meta: { userId, chatId, sessionId } },
219
+ { from: "ChatMe", to: "TypeScript", type: "built_with", meta: { userId, chatId, sessionId } },
220
+ ];
221
+
222
+ await memory.updateEntity(entities, relations, { userId, chatId, sessionId });
223
+
224
+ await memory.flushChat(sessionId, { wait: true });
225
+
226
+ const remembered = await memory.search({
227
+ query: "Alice 在 ChatMe 中负责什么?",
228
+ scope: "user",
229
+ scopeId: userId,
230
+ mode: "all",
231
+ limit: 5,
232
+ });
233
+
234
+ console.log(remembered);
235
+
236
+ const doc = await memory.addDocument({
237
+ content: `# ChatMe 技术说明
238
+
239
+ ChatMe 使用 TypeScript 构建,后端包含长期记忆系统。
240
+
241
+ ## Memory
242
+
243
+ 长期记忆系统使用 LanceDB 存储向量,使用 Grafeo 存储知识图谱。`,
244
+ userId,
245
+ chatId,
246
+ sessionId,
247
+ sourceName: "chatme-memory.md",
248
+ metadata: { product: "ChatMe" },
249
+ buildGraph: true,
250
+ wait: true,
251
+ waitGraph: true,
252
+ });
253
+
254
+ const knowledge = await memory.searchKnowledge({
255
+ query: "ChatMe 的长期记忆系统用了什么存储?",
256
+ scope: "chat",
257
+ scopeId: chatId,
258
+ mode: "auto",
259
+ });
260
+
261
+ console.log(doc.docId, knowledge);
262
+
263
+ const answer = await memory.ask({
264
+ query: "ChatMe 的长期记忆系统用了什么存储?",
265
+ scope: "chat",
266
+ scopeId: chatId,
267
+ includeKnowledge: true,
268
+ maxChars: 200,
269
+ });
270
+
271
+ console.log(answer);
272
+
273
+ memory.destroy();
274
+ ```
275
+
276
+ ## Configuration Reference
277
+
278
+ `MemoryManager` accepts a `MemoryConfig` object.
279
+
280
+ Required storage fields:
281
+
282
+ - `lancedbPath: string` - local LanceDB directory.
283
+ - `grafeoPath: string` - Grafeo database path.
284
+
285
+ Required LLM fields:
286
+
287
+ - `llmBaseUrl: string` - OpenAI-compatible base URL, for example `https://api.openai.com/v1`.
288
+ - `llmApiKey: string` - API key. Never hard-code real secrets in examples.
289
+ - `llmModel: string` - chat model used for summaries, graph decisions, entity extraction, and final `ask` answers.
290
+
291
+ Required embedding fields:
292
+
293
+ - `embeddingBaseUrl: string` - OpenAI-compatible embedding base URL.
294
+ - `embeddingApiKey: string` - embedding API key.
295
+ - `embeddingModel: string` - embedding model name.
296
+ - `embeddingDimension: number` - vector dimension. This must match the embedding model and existing LanceDB schema.
297
+
298
+ Embedding fallback behavior:
299
+
300
+ - If `embeddingBaseUrl` is empty, it falls back to `llmBaseUrl` and logs a warning.
301
+ - If `embeddingApiKey` is empty, it falls back to `llmApiKey` and logs a warning.
302
+ - This is convenient when one provider serves both chat and embeddings.
303
+
304
+ HTTP and retry fields:
305
+
306
+ - `httpTimeoutMs?: number` - timeout per LLM or embedding HTTP attempt. Default: `60000`.
307
+ - `httpMaxRetries?: number` - retry count after the first attempt for network errors, timeout/abort, HTTP `429`, and HTTP `5xx`. Default: `2`.
308
+ - `embeddingBatchSize?: number` - max texts per embedding request. Default: `20`.
309
+ - `embeddingConcurrency?: number` - concurrent embedding batches. Default: `2`.
310
+
311
+ Conversation memory fields:
312
+
313
+ - `sessionTokenLimit?: number` - when a session cache reaches this token count, compression is triggered. Default: `16386`.
314
+ - `historyWindowTokenLimit?: number` - maximum token budget for the rebuilt compressed history window. Default: `10240`.
315
+ - `topicRatio?: [number, number, number]` - number ratio for detail/summary/concise topics in the history window. Default: `[1, 5, 20]`.
316
+ - `detailMaxTokens?: number` - max detailed summary tokens requested from the LLM. Default: `2048`.
317
+ - `summaryMaxTokens?: number` - max medium summary tokens requested from the LLM. Default: `512`.
318
+ - `conciseMaxTokens?: number` - max concise summary tokens requested from the LLM. Default: `128`.
319
+ - `maxConcurrentCompressions?: number` - global semaphore limit for concurrent compression and knowledge ingestion tasks. Default: `3`.
320
+ - `entitySimilarityThreshold?: number` - graph entity similarity threshold. Default: `0.92`.
321
+ - `defaultSearchLimit?: number` - default `search` result limit. Default: `10`.
322
+ - `recallBoostMs?: number` - each topic recall count acts like this many milliseconds of freshness boost. Default: `3_600_000`.
323
+
324
+ Knowledge-base fields:
325
+
326
+ - `chunkStrategy?: "markdown-heading"` - current chunking strategy. Default: `"markdown-heading"`.
327
+ - `chunkMaxTokens?: number` - max tokens per chunk; long heading sections are split again. Default: `800`.
328
+ - `chunkOverlap?: number` - overlap tokens between chunks. Default: `0`.
329
+ - `knowledgeTopK?: number` - default `searchKnowledge` chunk return count. Default: `8`.
330
+ - `docCoarseTopK?: number` - document-level coarse recall count before chunk search. Set `0` to disable coarse recall. Default: `5`.
331
+ - `buildGraphDefault?: boolean | "auto"` - default graph build behavior for `addDocument`. Default: `"auto"`.
332
+ - `chunkRedundantIds?: boolean` - whether chunks redundantly store `userId`, `chatId`, and `sessionId` for direct filtering. Default: `true`.
333
+ - `knowledgeGraphTriggerScore?: number` - in `mode: "auto"`, graph expansion only starts when top chunk cosine score reaches this threshold. Default: `0.78`.
334
+ - `knowledgeGraphEntityTopK?: number` - entity vector search candidate count for knowledge graph expansion. Default: `10`.
335
+ - `knowledgeGraphAnchorTopK?: number` - top entity anchors used for one-hop expansion. Default: `3`.
336
+ - `knowledgeGraphHopLimit?: number` - max one-hop relations per anchor. Default: `10`.
337
+ - `graphExtractConcurrency?: number` - concurrent chunk-level entity extraction tasks during document graph construction. Default: `2`.
338
+ - `graphBuildTimeoutMs?: number` - timeout used only when a caller explicitly waits for graph construction with `waitGraph: true`. Default: `120000`.
339
+
340
+ ## Critical Async And Wait Semantics
341
+
342
+ The package intentionally separates durable base writes from expensive graph construction.
343
+
344
+ `updateChat(messages, opts)`:
345
+
346
+ - Embeds and writes raw messages to LanceDB before resolving.
347
+ - Creates or updates the session record.
348
+ - Updates the in-memory session cache.
349
+ - If `sessionTokenLimit` is exceeded, compression is triggered in the background.
350
+ - Background compression may create topics/history and graph entities later.
351
+
352
+ `flushChat(sessionId?, opts?)`:
353
+
354
+ - Forces compression for the current cached messages even if `sessionTokenLimit` was not reached.
355
+ - `wait: true` waits for topic creation, cache clearing, and history-window rebuild.
356
+ - `wait: true` does not wait for graph extraction/persistence.
357
+ - `waitGraph: true` waits for graph extraction/persistence too.
358
+ - When `waitGraph: true`, graph waiting is bounded by `graphBuildTimeoutMs` and may throw a timeout error.
359
+
360
+ `addDocument(opts)`:
361
+
362
+ - Writes the document row first.
363
+ - Chunk embedding and chunk insertion run through the ingestion pipeline.
364
+ - Graph construction, when enabled, runs after chunks are ready.
365
+ - With neither `wait` nor `waitGraph`, the method returns after document storage; chunks and graph are background work.
366
+ - `wait: true` waits until chunks are inserted and searchable.
367
+ - `wait: true` does not wait for graph construction.
368
+ - `waitGraph: true` implies waiting for chunks and then waits for graph construction.
369
+ - When `waitGraph: true`, graph waiting is bounded by `graphBuildTimeoutMs` and may throw a timeout error.
370
+
371
+ Use `wait: true` when the next line of code must immediately call `searchKnowledge` and find chunks. Use `waitGraph: true` only when `hasGraph` or graph hits must be ready immediately.
372
+
373
+ ## Data Model
374
+
375
+ `RawMessage` is input to `updateChat`:
376
+
377
+ - `messageId?: string` - generated with UUID if omitted.
378
+ - `talkerId?: string` - speaker id. Default: `"user"`.
379
+ - `chatId?: string` - conversation/chat/agent id.
380
+ - `userId?: string` - owner user id.
381
+ - `sessionId?: string` - session id.
382
+ - `type?: "text" | "image" | "file"` - default: `"text"`.
383
+ - `content: string` - required plain text used for embedding and retrieval.
384
+ - `parts?: ContentPart[]` - optional multimodal content parts; stored serialized.
385
+ - `usage?: number` - token count; estimated with `tiktoken` if omitted.
386
+ - `metadata?: Record<string, unknown>` - custom metadata stored as JSON.
387
+ - `createdAt?: number` - Unix milliseconds; default is current time.
388
+
389
+ `ContentPart` aligns with common multimodal content shapes:
390
+
391
+ - Text: `{ type: "text", text: "..." }`
392
+ - Image: `{ type: "image_url", image_url: { url: "https://..." } }`
393
+ - File: `{ type: "file_url", file_url: { url: "https://...", name: "file.pdf" } }`
394
+
395
+ `StoredMessage` is persisted message output with required ids, serialized `parts`, serialized `metadata`, generated vector, and `createdAt`.
396
+
397
+ `Topic` is a compressed memory item:
398
+
399
+ - `title` - short topic title.
400
+ - `detail` - rich summary.
401
+ - `summary` - medium summary.
402
+ - `concise` - short summary.
403
+ - `startTime` and `endTime` - covered message time range.
404
+ - `recallCount` - affects freshness ranking through `recallBoostMs`.
405
+
406
+ `Fact` is a user/chat scoped manual memory:
407
+
408
+ - `level: "user" | "chat"`
409
+ - `userId`, `chatId`, optional `sessionId`
410
+ - `content`
411
+ - `createdAt`
412
+
413
+ `Entity` and `Relation` are graph records:
414
+
415
+ - Entity: `{ name, type, meta }`
416
+ - Relation: `{ from, to, type, happenedAt?, meta }`
417
+ - `meta` should usually include `userId`, `chatId`, `sessionId`, and source details.
418
+
419
+ `SessionView` is the in-memory session representation:
420
+
421
+ - `sessionId`
422
+ - `chatId`
423
+ - `userId`
424
+ - `title`
425
+ - `metadata: Record<string, unknown>`
426
+ - `createdAt`
427
+ - `updatedAt`
428
+
429
+ `Document` is a knowledge-base document row:
430
+
431
+ - `docId`
432
+ - `userId`, `chatId`, `sessionId`
433
+ - `title`, `sourceName`
434
+ - `fullContent`
435
+ - `contentHash`
436
+ - `summary`
437
+ - `summaryVector`
438
+ - `chunkCount`
439
+ - `hasGraph`
440
+ - `metadata`
441
+ - `createdAt`, `updatedAt`
442
+
443
+ `Chunk` is a knowledge-base chunk row:
444
+
445
+ - `chunkId`
446
+ - `docId`
447
+ - `content`
448
+ - `headingPath`
449
+ - `ordinal`
450
+ - `tokens`
451
+ - `vector`
452
+ - optional redundant domain ids depending on `chunkRedundantIds`
453
+
454
+ ## Scope And Search Modes
455
+
456
+ Scopes:
457
+
458
+ - `scope: "session"` - filter by `sessionId`; pass `scopeId`.
459
+ - `scope: "chat"` - filter by `chatId`; pass `scopeId`.
460
+ - `scope: "user"` - filter by `userId`; pass `scopeId`.
461
+ - `scope: "all"` - no domain filter; `scopeId` is ignored.
462
+
463
+ Search modes:
464
+
465
+ - `mode: "fast"` - only LanceDB hybrid/vector retrieval. No graph search.
466
+ - `mode: "auto"` - conversation search asks the LLM whether graph search is needed; knowledge search triggers graph expansion only when top chunk score is high enough.
467
+ - `mode: "all"` - include graph search and merge graph results with vector/full-text results.
468
+
469
+ For deterministic low-latency calls, use `fast`. For richer entity relationship answers, use `auto` or `all`.
470
+
471
+ ## API Reference
472
+
473
+ ### `new MemoryManager(config)`
474
+
475
+ Creates the manager and resolves defaults. It does not connect to storage until `init()`.
476
+
477
+ ### `init(): Promise<void>`
478
+
479
+ Initializes tiktoken, LanceDB, Grafeo, facts cache, sessions cache, and restored history windows. Always call before using read/write APIs.
480
+
481
+ ### `updateChat(messages, opts?): Promise<void>`
482
+
483
+ Stores new chat messages.
484
+
485
+ Options:
486
+
487
+ - `userId?: string`
488
+ - `chatId?: string`
489
+ - `sessionId?: string`
490
+ - `sessionTitle?: string`
491
+ - `sessionMetadata?: Record<string, unknown>`
492
+
493
+ Defaults for ids are `"default"`.
494
+
495
+ Use for every new user/assistant message that should become retrievable memory.
496
+
497
+ ### `flushChat(sessionId?, opts?): Promise<void>`
498
+
499
+ Forces compression of a session cache.
500
+
501
+ Options:
502
+
503
+ - `wait?: boolean` - wait for topic/history work.
504
+ - `waitGraph?: boolean` - also wait for graph extraction/persistence, bounded by `graphBuildTimeoutMs`.
505
+
506
+ If `sessionId` is omitted, `"default"` is used.
507
+
508
+ ### `search(opts): Promise<SearchResult[]>`
509
+
510
+ Searches conversation memory.
511
+
512
+ Options:
513
+
514
+ - `query: string`
515
+ - `scope?: "session" | "chat" | "user" | "all"`
516
+ - `scopeId?: string`
517
+ - `mode?: "fast" | "auto" | "all"`
518
+ - `limit?: number`
519
+
520
+ Returns results with:
521
+
522
+ - `type: "message" | "topic" | "entity" | "relation"`
523
+ - `content`
524
+ - `score`
525
+ - `meta`
526
+
527
+ ### `ask(opts): Promise<string>`
528
+
529
+ Searches memory and asks the configured LLM to summarize the hits into a natural-language answer.
530
+
531
+ Options are `SearchOptions` plus:
532
+
533
+ - `maxChars?: number`
534
+ - `includeKnowledge?: boolean`
535
+
536
+ When `includeKnowledge: true`, the method also calls `searchKnowledge` and merges knowledge chunks and graph hits into the answer context.
537
+
538
+ ### `updateFacts(content, level, userId, chatId, sessionId?): Promise<void>`
539
+
540
+ Adds a fact to persistent storage and in-memory cache.
541
+
542
+ - `level` is `"user"` or `"chat"`.
543
+ - User facts apply across chats for that user.
544
+ - Chat facts apply to a specific chat/agent context.
545
+
546
+ ### `getFacts(level, id): Promise<string>`
547
+
548
+ Returns facts as newline-separated text in `ISO_TIME:content` format. Use `level: "user"` with `userId`, or `level: "chat"` with `chatId`.
549
+
550
+ ### `getFactsForContext(userId, chatId): Promise<string>`
551
+
552
+ Returns merged user-level facts for `userId` and chat-level facts for `chatId`, sorted by time. This is useful for injecting stable facts into an agent prompt.
553
+
554
+ ### `addFact(content, level, userId, chatId, sessionId?): Promise<void>`
555
+
556
+ Alias-style management API for adding a fact manually.
557
+
558
+ ### `deleteFact(factId): Promise<boolean>`
559
+
560
+ Deletes one fact and returns whether it was found.
561
+
562
+ ### `updateEntity(entities, relations, context?): Promise<void>`
563
+
564
+ Upserts graph entities and relations. The method embeds entity names and stores graph data in Grafeo.
565
+
566
+ Context:
567
+
568
+ - `sessionId?: string`
569
+ - `chatId?: string`
570
+ - `userId?: string`
571
+
572
+ ### `getHistoryWindow(sessionId): string`
573
+
574
+ Returns the compressed history window for a session. This is prompt-ready text built from recent `Topic` records using `topicRatio` and `historyWindowTokenLimit`.
575
+
576
+ ### `getRecentMessages(sessionId, limit): Promise<StoredMessage[]>`
577
+
578
+ Returns recent messages for a session.
579
+
580
+ ### Session APIs
581
+
582
+ - `getSession(sessionId): SessionView | null`
583
+ - `getSessionsByUserId(userId, opts?): SessionView[]`
584
+ - `getSessionsByChatId(chatId, opts?): SessionView[]`
585
+ - `searchSessions(opts): SessionView[]`
586
+ - `updateSession(sessionId, opts): Promise<SessionView | null>`
587
+ - `deleteSession(sessionId): Promise<boolean>`
588
+ - `listSessions(filter?): SessionView[]`
589
+
590
+ Session update options:
591
+
592
+ - `title?: string`
593
+ - `metadata?: Record<string, unknown>`
594
+
595
+ Session search options:
596
+
597
+ - `title?: string`
598
+ - `userId?: string`
599
+ - `chatId?: string`
600
+ - `limit?: number`
601
+
602
+ ### Management APIs
603
+
604
+ - `stats(): Promise<{ sessions; messages; topics; facts; entities; relations; documents; chunks }>` - total counts.
605
+ - `trend(days?): Promise<Array<{ date; sessions; messages; facts }>>` - daily activity trend. `days` defaults to `30` and is clamped to `1..365`.
606
+ - `listMessages(sessionId, limit?): Promise<StoredMessage[]>`
607
+ - `listTopics(filter?): Promise<Topic[]>`
608
+ - `listFacts(filter?): Fact[]`
609
+ - `listEntities(): Promise<Entity[]>`
610
+ - `listRelations(): Promise<Relation[]>`
611
+
612
+ ### `addDocument(opts): Promise<{ docId: string }>`
613
+
614
+ Ingests a Markdown document into the knowledge base.
615
+
616
+ Options:
617
+
618
+ - `content: string` - required Markdown content.
619
+ - `userId?: string`
620
+ - `chatId?: string`
621
+ - `sessionId?: string`
622
+ - `title?: string` - inferred from first Markdown heading or first non-empty line when omitted.
623
+ - `sourceName?: string`
624
+ - `metadata?: Record<string, unknown>`
625
+ - `buildGraph?: boolean | "auto"` - default from `config.buildGraphDefault`.
626
+ - `wait?: boolean` - wait for chunks to be embedded and inserted.
627
+ - `waitGraph?: boolean` - wait for chunks and graph construction.
628
+
629
+ Deduplication:
630
+
631
+ - Documents are deduplicated by `sha256(content)` within the same `userId + chatId + sessionId` domain.
632
+ - If a duplicate exists, `addDocument` returns the existing `docId`.
633
+
634
+ Graph behavior:
635
+
636
+ - `buildGraph: false` disables graph construction.
637
+ - `buildGraph: true` builds a graph.
638
+ - `buildGraph: "auto"` uses the LLM document summary call to decide whether the document has valuable entity relationships.
639
+
640
+ ### `searchKnowledge(opts): Promise<KnowledgeSearchResult>`
641
+
642
+ Searches Markdown knowledge-base chunks.
643
+
644
+ Options:
645
+
646
+ - `query: string`
647
+ - `scope?: "session" | "chat" | "user" | "all"`
648
+ - `scopeId?: string`
649
+ - `mode?: "fast" | "auto" | "all"`
650
+ - `limit?: number`
651
+
652
+ Returns:
653
+
654
+ - `chunks: Array<{ chunkId; docId; content; headingPath; score }>`
655
+ - `documents: Array<{ docId; title; matchedChunkCount }>`
656
+ - `graphHits: SearchResult[]`
657
+
658
+ The method uses document-level coarse recall when `docCoarseTopK > 0`, then chunk-level search. In `auto` mode, knowledge graph expansion only happens when the top chunk score reaches `knowledgeGraphTriggerScore`.
659
+
660
+ ### `getDocument(docId): Promise<Document | null>`
661
+
662
+ Returns a document including `fullContent`.
663
+
664
+ ### `deleteDocument(docId): Promise<boolean>`
665
+
666
+ Deletes the document, its chunks, and best-effort Grafeo knowledge graph entries for that document.
667
+
668
+ ### `listDocuments(filter?): Promise<Document[]>`
669
+
670
+ Lists documents, optionally filtered by:
671
+
672
+ - `userId`
673
+ - `chatId`
674
+ - `sessionId`
675
+
676
+ ### `destroy(): void`
677
+
678
+ Closes the Grafeo connection. It does not delete LanceDB or Grafeo data.
679
+
680
+ ## Common Recipes
681
+
682
+ ### Store User And Assistant Messages
683
+
684
+ ```ts
685
+ await memory.updateChat(
686
+ [
687
+ { talkerId: "user", content: "我喜欢 TypeScript,不太喜欢写纯 JavaScript。" },
688
+ { talkerId: "assistant", content: "记住了,你偏好 TypeScript。" },
689
+ ],
690
+ { userId: "u1", chatId: "assistant-main", sessionId: "s1" }
691
+ );
692
+ ```
693
+
694
+ ### Force A Prompt-Ready History Window
695
+
696
+ ```ts
697
+ await memory.flushChat("s1", { wait: true });
698
+ const history = memory.getHistoryWindow("s1");
699
+
700
+ const systemPrompt = `你是助手。以下是长期记忆摘要:\n${history}`;
701
+ ```
702
+
703
+ ### Wait For Conversation Graph Data
704
+
705
+ ```ts
706
+ await memory.flushChat("s1", {
707
+ wait: true,
708
+ waitGraph: true,
709
+ });
710
+ ```
711
+
712
+ Use this only when graph data must be ready immediately. It can throw if graph construction exceeds `graphBuildTimeoutMs`.
713
+
714
+ ### Store Stable Facts
715
+
716
+ ```ts
717
+ await memory.updateFacts("用户偏好 TypeScript", "user", "u1", "assistant-main", "s1");
718
+
719
+ const facts = await memory.getFactsForContext("u1", "assistant-main");
720
+ ```
721
+
722
+ ### Search Fast Without Graph
723
+
724
+ ```ts
725
+ const hits = await memory.search({
726
+ query: "用户偏好什么语言?",
727
+ scope: "user",
728
+ scopeId: "u1",
729
+ mode: "fast",
730
+ });
731
+ ```
732
+
733
+ ### Search With Graph
734
+
735
+ ```ts
736
+ const hits = await memory.search({
737
+ query: "Alice 负责哪个项目,项目用了什么技术?",
738
+ scope: "chat",
739
+ scopeId: "work-agent",
740
+ mode: "all",
741
+ });
742
+ ```
743
+
744
+ ### Ingest Markdown And Search Immediately
745
+
746
+ ```ts
747
+ const { docId } = await memory.addDocument({
748
+ content: "# Rust\n\nRust 的所有权系统保证内存安全。",
749
+ userId: "u1",
750
+ chatId: "dev-agent",
751
+ sourceName: "rust.md",
752
+ wait: true,
753
+ });
754
+
755
+ const result = await memory.searchKnowledge({
756
+ query: "Rust 如何保证内存安全?",
757
+ scope: "chat",
758
+ scopeId: "dev-agent",
759
+ mode: "fast",
760
+ });
761
+ ```
762
+
763
+ ### Ingest Markdown And Wait For Knowledge Graph
764
+
765
+ ```ts
766
+ await memory.addDocument({
767
+ content: markdown,
768
+ userId: "u1",
769
+ chatId: "work-agent",
770
+ sourceName: "project.md",
771
+ buildGraph: true,
772
+ waitGraph: true,
773
+ });
774
+ ```
775
+
776
+ ### Ask With Conversation Memory And Knowledge Base
777
+
778
+ ```ts
779
+ const answer = await memory.ask({
780
+ query: "这个项目的技术栈是什么?结合我之前说过的内容回答。",
781
+ scope: "chat",
782
+ scopeId: "work-agent",
783
+ mode: "auto",
784
+ includeKnowledge: true,
785
+ maxChars: 300,
786
+ });
787
+ ```
788
+
789
+ ## Node And Relation Types
790
+
791
+ Default node types:
792
+
793
+ `Person`, `Group`, `Organization`, `Project`, `Task`, `Decision`, `Plan`, `Event`, `Product`, `Technology`, `Data`, `Document`, `Topic`, `Concept`, `Preference`, `Habit`, `Goal`, `Skill`, `Attribute`, `Value`, `Status`, `Time`, `Location`, `Resource`, `Relationship`.
794
+
795
+ Default relation types:
796
+
797
+ `is_a`, `part_of`, `belongs_to`, `contains`, `has_member`, `mentioned_in`, `refers_to`, `same_as`, `alias_of`, `related_to`, `associated_with`, `uses`, `creates`, `updates`, `buys`, `owns`, `consumes`, `works_on`, `prefers`, `likes`, `dislikes`, `interested_in`, `favorite`, `plans`, `decides`, `habit_of`, `tends_to`, `avoids`, `skilled_in`, `learning`, `knows`, `friends_with`, `married_to`, `parent_of`, `child_of`, `lives_with`, `depends_on`, `built_with`, `integrates_with`, `deployed_on`, `inputs`, `outputs`, `trained_on`, `predicts`, `happens_at`, `started_at`, `ended_at`, `affects`, `causes`, `leads_to`, `improves`, `reduces`, `assigned_to`, `executed_by`, `blocks`, `completes`, `describes`, `explains`, `references`.
798
+
799
+ Graph kind constants:
800
+
801
+ - `KIND_CONVERSATION = "conversation"`
802
+ - `KIND_KNOWLEDGE = "knowledge"`
803
+
804
+ Conversation graph search only targets conversation graph data. Knowledge graph search only targets knowledge graph data.
805
+
806
+ ## Best Practices
807
+
808
+ - Always call `await memory.init()` before using the manager.
809
+ - Always call `memory.destroy()` during shutdown to close Grafeo resources.
810
+ - Use stable `userId`, `chatId`, and `sessionId` values. They define memory isolation and retrieval scope.
811
+ - Use `scope: "user"` for personal memory, `scope: "chat"` for one agent/conversation channel, and `scope: "session"` for one short-lived conversation.
812
+ - Use `getFactsForContext` for stable facts and `getHistoryWindow` for compressed session history.
813
+ - Use `updateFacts` for explicit facts that should not depend on LLM extraction.
814
+ - Use `updateEntity` when you already know structured entities and relations.
815
+ - Use `flushChat(..., { wait: true })` before reading `getHistoryWindow` in tests or immediate workflows.
816
+ - Use `addDocument(..., { wait: true })` before immediate knowledge search.
817
+ - Use `waitGraph: true` sparingly because graph extraction depends on LLM calls and can be slower.
818
+ - Keep `embeddingDimension` unchanged for an existing LanceDB directory unless you rebuild the database.
819
+ - Keep examples free of real API keys, tokens, personal data, and proprietary customer content.
820
+
821
+ ## Common Pitfalls
822
+
823
+ - Forgetting `init()` before `updateChat` or search.
824
+ - Expecting `updateChat` to finish compression or graph extraction. It only waits for raw message storage.
825
+ - Expecting `flushChat(..., { wait: true })` to wait for graph data. Use `waitGraph: true`.
826
+ - Expecting `addDocument(..., { wait: true })` to wait for graph data. Use `waitGraph: true`.
827
+ - Setting `waitGraph: true` without allowing enough `graphBuildTimeoutMs` for large documents.
828
+ - Using `scope: "session"` with a `chatId`, or `scope: "chat"` with a `userId`. `scopeId` must match the selected scope.
829
+ - Searching immediately after `addDocument` without `wait: true`; chunks may still be ingesting.
830
+ - Mixing embedding models with different vector dimensions in the same existing LanceDB path.
831
+ - Treating `metadata` as indexed arbitrary JSON. Store important filter dimensions in stable top-level ids where possible.
832
+ - Using `mode: "all"` for every query. It is richer but can be slower than `fast`.
833
+
834
+ ## Security And Privacy
835
+
836
+ - Never hard-code real `llmApiKey` or `embeddingApiKey`.
837
+ - Treat LanceDB and Grafeo files as sensitive data stores.
838
+ - `content`, `metadata`, facts, and graph `meta` may contain private information.
839
+ - `destroy()` closes resources; it does not scrub data.
840
+ - Implement your own retention/deletion policy around `deleteSession`, `deleteDocument`, `deleteFact`, and storage directory cleanup.
841
+ - If exposing memory search through an API, enforce user authorization before passing `scope` and `scopeId`.
842
+
843
+ ## Publication Notes
844
+
845
+ This file is included in the npm package root through the `files` field in `packages/memory/package.json`. When public APIs, config defaults, wait behavior, search semantics, or examples change, update this `llms.txt` in the same change.