@ppagent/memory 0.4.1 → 0.4.4

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.
package/dist/cli.js CHANGED
@@ -159,6 +159,7 @@ function tableDefs(dim) {
159
159
  { name: "parts", type: "text", nullable: true },
160
160
  // 兼容存量数据
161
161
  { name: "payload", type: "text", nullable: true },
162
+ { name: "context_payload", type: "text", nullable: true },
162
163
  { name: "vector", type: "vector" },
163
164
  { name: "usage", type: "int" },
164
165
  { name: "metadata", type: "text" },
@@ -290,6 +291,7 @@ function messageToRow(m) {
290
291
  content: m.content,
291
292
  parts: m.parts ?? "[]",
292
293
  payload: m.payload ?? null,
294
+ context_payload: m.contextPayload ?? null,
293
295
  vector: m.vector,
294
296
  usage: m.usage,
295
297
  metadata: m.metadata,
@@ -308,6 +310,7 @@ function rowToMessage(r) {
308
310
  parts: r.parts ?? "[]",
309
311
  // 旧数据无此列时安全降级
310
312
  payload: r.payload ?? void 0,
313
+ contextPayload: r.context_payload ?? void 0,
311
314
  vector: toVector(r.vector),
312
315
  usage: positiveNumber(r.usage),
313
316
  metadata: r.metadata,
@@ -594,7 +597,13 @@ var MemoryStore = class {
594
597
  report.messagesScanned = messageRows.length;
595
598
  for (const row of messageRows) {
596
599
  const usage2 = countTokens(
597
- [row.content, row.parts ?? "[]", row.metadata ?? "{}", row.payload ?? ""].map(String).join("\n")
600
+ [
601
+ row.content,
602
+ row.parts ?? "[]",
603
+ row.metadata ?? "{}",
604
+ row.payload ?? "",
605
+ row.context_payload ?? ""
606
+ ].map(String).join("\n")
598
607
  );
599
608
  if (Number(row.usage) === usage2) continue;
600
609
  await this.provider.update(MESSAGES_TABLE, { usage: usage2 }, [
package/dist/index.d.ts CHANGED
@@ -61,6 +61,12 @@ interface RawMessage {
61
61
  parts?: ContentPart[];
62
62
  /** 宿主框架的原始消息载荷;记忆框架仅透明保存和回读。 */
63
63
  payload?: unknown;
64
+ /**
65
+ * 仅用于下一次模型上下文重放的宿主载荷。
66
+ * 会透明保存、原样回读并计入原始窗口 token,但不会进入 embedding、全文检索、
67
+ * Topic 摘要或知识图谱抽取。适合保存 provider-ready 工具协议消息等大体积细节。
68
+ */
69
+ contextPayload?: unknown;
64
70
  usage?: number;
65
71
  metadata?: Record<string, unknown>;
66
72
  createdAt?: number;
@@ -78,6 +84,8 @@ interface StoredMessage {
78
84
  parts: string;
79
85
  /** JSON.stringify(payload),未提供时为 undefined。 */
80
86
  payload?: string;
87
+ /** JSON.stringify(contextPayload),未提供时为 undefined。 */
88
+ contextPayload?: string;
81
89
  usage: number;
82
90
  metadata: string;
83
91
  vector: number[];
@@ -236,6 +244,7 @@ interface MemoryRawMessage {
236
244
  content: string;
237
245
  parts?: ContentPart[];
238
246
  payload?: unknown;
247
+ contextPayload?: unknown;
239
248
  usage: number;
240
249
  metadata?: Record<string, unknown>;
241
250
  createdAt: number;
@@ -261,6 +270,11 @@ interface GetHistoryWindowOptions {
261
270
  /** 固定预算的压缩记忆 + 近期未压缩原始消息。 */
262
271
  interface MemoryContextWindow {
263
272
  sessionId: string;
273
+ /**
274
+ * 当前压缩边界的轻量版本标识。后台/阻塞压缩或 Topic 归并改变窗口时随之变化;
275
+ * 新增原始消息本身不保证改变该值。
276
+ */
277
+ compressionRevision?: string;
264
278
  compressedContext: string;
265
279
  recentMessages: MemoryRawMessage[];
266
280
  usage: MemoryContextWindowUsage;
package/dist/index.js CHANGED
@@ -827,6 +827,7 @@ function tableDefs(dim) {
827
827
  { name: "parts", type: "text", nullable: true },
828
828
  // 兼容存量数据
829
829
  { name: "payload", type: "text", nullable: true },
830
+ { name: "context_payload", type: "text", nullable: true },
830
831
  { name: "vector", type: "vector" },
831
832
  { name: "usage", type: "int" },
832
833
  { name: "metadata", type: "text" },
@@ -958,6 +959,7 @@ function messageToRow(m) {
958
959
  content: m.content,
959
960
  parts: m.parts ?? "[]",
960
961
  payload: m.payload ?? null,
962
+ context_payload: m.contextPayload ?? null,
961
963
  vector: m.vector,
962
964
  usage: m.usage,
963
965
  metadata: m.metadata,
@@ -976,6 +978,7 @@ function rowToMessage(r) {
976
978
  parts: r.parts ?? "[]",
977
979
  // 旧数据无此列时安全降级
978
980
  payload: r.payload ?? void 0,
981
+ contextPayload: r.context_payload ?? void 0,
979
982
  vector: toVector(r.vector),
980
983
  usage: positiveNumber(r.usage),
981
984
  metadata: r.metadata,
@@ -1262,7 +1265,13 @@ var MemoryStore = class {
1262
1265
  report.messagesScanned = messageRows.length;
1263
1266
  for (const row of messageRows) {
1264
1267
  const usage = countTokens(
1265
- [row.content, row.parts ?? "[]", row.metadata ?? "{}", row.payload ?? ""].map(String).join("\n")
1268
+ [
1269
+ row.content,
1270
+ row.parts ?? "[]",
1271
+ row.metadata ?? "{}",
1272
+ row.payload ?? "",
1273
+ row.context_payload ?? ""
1274
+ ].map(String).join("\n")
1266
1275
  );
1267
1276
  if (Number(row.usage) === usage) continue;
1268
1277
  await this.provider.update(MESSAGES_TABLE, { usage }, [
@@ -1874,7 +1883,7 @@ var LlmService = class {
1874
1883
 
1875
1884
  \u8F93\u51FA\u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u5305\u542B\u4EFB\u4F55 markdown \u6807\u8BB0\uFF0C\u683C\u5F0F\u5982\u4E0B\uFF1A
1876
1885
  {"title": "...", "summary": "..."}`;
1877
- const formatted = messages.map((m) => formatStoredMessage(m)).join("\n");
1886
+ const formatted = messages.map((m) => formatStoredMessageForCompression(m)).join("\n");
1878
1887
  const response = await this.chatCompletionWithUsage(
1879
1888
  systemPrompt,
1880
1889
  `\u8BF7\u538B\u7F29\u4EE5\u4E0B\u5BF9\u8BDD\uFF1A
@@ -2039,11 +2048,81 @@ ${text}`
2039
2048
  ${context}`, false);
2040
2049
  }
2041
2050
  };
2042
- function formatStoredMessage(message) {
2043
- const extras = [message.parts, message.metadata, message.payload].filter((value) => value && value !== "[]" && value !== "{}").join(" ");
2051
+ function formatStoredMessageForCompression(message) {
2052
+ const metadata = stripToolCallContentFromMetadata(message.metadata);
2053
+ const extras = [message.parts, metadata, message.payload].filter((value) => value && value !== "[]" && value !== "{}").join(" ");
2044
2054
  return `[${new Date(message.createdAt).toISOString()}] ${message.talkerId || "user"}: ${message.content}${extras ? `
2045
2055
  meta=${extras}` : ""}`;
2046
2056
  }
2057
+ var OMIT_COMPRESSION_VALUE = Symbol("omit-compression-value");
2058
+ var TOOL_CALL_CONTAINER_KEYS = /* @__PURE__ */ new Set([
2059
+ "toolcall",
2060
+ "toolcalls",
2061
+ "toolresult",
2062
+ "toolresults",
2063
+ "tooloutput",
2064
+ "tooloutputs",
2065
+ "toolresponse",
2066
+ "toolresponses",
2067
+ "tooluse",
2068
+ "tooluses",
2069
+ "functioncall",
2070
+ "functioncalls",
2071
+ "functionresponse",
2072
+ "functionresponses"
2073
+ ]);
2074
+ var TOOL_CALL_ID_KEYS = /* @__PURE__ */ new Set(["toolcallid", "tooluseid"]);
2075
+ var TOOL_BLOCK_TYPES = /* @__PURE__ */ new Set([
2076
+ "toolcall",
2077
+ "tooloutput",
2078
+ "toolresponse",
2079
+ "toolresult",
2080
+ "tooluse",
2081
+ "functioncall",
2082
+ "functionresponse"
2083
+ ]);
2084
+ function stripToolCallContentFromMetadata(metadata) {
2085
+ if (!metadata || metadata === "{}") return metadata;
2086
+ try {
2087
+ const parsed = JSON.parse(metadata);
2088
+ const sanitized = stripToolCallContent(parsed);
2089
+ return JSON.stringify(sanitized === OMIT_COMPRESSION_VALUE ? {} : sanitized);
2090
+ } catch {
2091
+ return metadata;
2092
+ }
2093
+ }
2094
+ function stripToolCallContent(value) {
2095
+ if (Array.isArray(value)) {
2096
+ return value.map((item) => stripToolCallContent(item)).filter((item) => item !== OMIT_COMPRESSION_VALUE);
2097
+ }
2098
+ if (!isRecord(value)) return value;
2099
+ if (isToolCallBlock(value)) return OMIT_COMPRESSION_VALUE;
2100
+ const sanitized = {};
2101
+ for (const [key, child] of Object.entries(value)) {
2102
+ const normalizedKey = normalizeToolKey(key);
2103
+ if (TOOL_CALL_CONTAINER_KEYS.has(normalizedKey) || TOOL_CALL_ID_KEYS.has(normalizedKey)) {
2104
+ continue;
2105
+ }
2106
+ const next = stripToolCallContent(child);
2107
+ if (next !== OMIT_COMPRESSION_VALUE) sanitized[key] = next;
2108
+ }
2109
+ return sanitized;
2110
+ }
2111
+ function isToolCallBlock(value) {
2112
+ const role = typeof value.role === "string" ? normalizeToolKey(value.role) : "";
2113
+ const type = typeof value.type === "string" ? normalizeToolKey(value.type) : "";
2114
+ if (role === "tool" || role === "function") return true;
2115
+ if (TOOL_BLOCK_TYPES.has(type)) return true;
2116
+ const normalizedKeys = new Set(Object.keys(value).map(normalizeToolKey));
2117
+ const hasToolCallId = [...TOOL_CALL_ID_KEYS].some((key) => normalizedKeys.has(key));
2118
+ return hasToolCallId && ["content", "output", "result", "response"].some((key) => normalizedKeys.has(key));
2119
+ }
2120
+ function normalizeToolKey(value) {
2121
+ return value.replace(/[\s_-]/g, "").toLowerCase();
2122
+ }
2123
+ function isRecord(value) {
2124
+ return value !== null && typeof value === "object";
2125
+ }
2047
2126
 
2048
2127
  // src/manager/compress.manager.ts
2049
2128
  import { v4 as uuidv4 } from "uuid";
@@ -3073,7 +3152,7 @@ function effectiveUsage(message) {
3073
3152
  if (Number.isFinite(message.usage) && message.usage > 0) return Math.floor(message.usage);
3074
3153
  return Math.max(
3075
3154
  1,
3076
- (message.content.length + (message.parts?.length ?? 0) + message.metadata.length + (message.payload?.length ?? 0)) * 2
3155
+ (message.content.length + (message.parts?.length ?? 0) + message.metadata.length + (message.payload?.length ?? 0) + (message.contextPayload?.length ?? 0)) * 2
3077
3156
  );
3078
3157
  }
3079
3158
  function sumMessageTokens(messages) {
@@ -3264,7 +3343,8 @@ var MemoryManager = class {
3264
3343
  const parts = JSON.stringify(message.parts ?? []);
3265
3344
  const metadata = JSON.stringify(message.metadata ?? {});
3266
3345
  const payload = message.payload === void 0 ? void 0 : JSON.stringify(message.payload);
3267
- const tokenInput = [message.content, parts, metadata, payload ?? ""].join("\n");
3346
+ const contextPayload = message.contextPayload === void 0 ? void 0 : JSON.stringify(message.contextPayload);
3347
+ const tokenInput = [message.content, parts, metadata, payload ?? "", contextPayload ?? ""].join("\n");
3268
3348
  return {
3269
3349
  messageId: message.messageId ?? uuidv44(),
3270
3350
  talkerId: message.talkerId ?? "user",
@@ -3275,6 +3355,7 @@ var MemoryManager = class {
3275
3355
  content: message.content,
3276
3356
  parts,
3277
3357
  payload,
3358
+ contextPayload,
3278
3359
  usage: message.usage ?? countTokens(tokenInput),
3279
3360
  metadata,
3280
3361
  createdAt: message.createdAt ?? now
@@ -3555,6 +3636,7 @@ var MemoryManager = class {
3555
3636
  }
3556
3637
  return {
3557
3638
  sessionId,
3639
+ compressionRevision: buildCompressionRevision(topics, entry.messages),
3558
3640
  compressedContext: this.sessionCache.buildCompressedContext(sessionId, topics),
3559
3641
  recentMessages: entry.messages.map(toMemoryRawMessage),
3560
3642
  usage: {
@@ -3815,6 +3897,7 @@ var MemoryManager = class {
3815
3897
  function toMemoryRawMessage(message) {
3816
3898
  const parts = safeParseArray(message.parts);
3817
3899
  const payload = message.payload === void 0 ? void 0 : safeParseValue(message.payload);
3900
+ const contextPayload = message.contextPayload === void 0 ? void 0 : safeParseValue(message.contextPayload);
3818
3901
  return {
3819
3902
  messageId: message.messageId,
3820
3903
  talkerId: message.talkerId,
@@ -3825,11 +3908,22 @@ function toMemoryRawMessage(message) {
3825
3908
  content: message.content,
3826
3909
  ...parts.length > 0 && { parts },
3827
3910
  ...payload !== void 0 && { payload },
3911
+ ...contextPayload !== void 0 && { contextPayload },
3828
3912
  usage: message.usage,
3829
3913
  metadata: safeParseObject2(message.metadata),
3830
3914
  createdAt: message.createdAt
3831
3915
  };
3832
3916
  }
3917
+ function buildCompressionRevision(topics, messages) {
3918
+ const lastTopic = topics.at(-1);
3919
+ const firstRaw = messages.at(0);
3920
+ return [
3921
+ topics.length,
3922
+ lastTopic?.summaryId ?? "none",
3923
+ lastTopic?.updatedAt ?? 0,
3924
+ firstRaw?.messageId ?? "none"
3925
+ ].join(":");
3926
+ }
3833
3927
  function sumTopicTokens2(topics) {
3834
3928
  return topics.reduce((sum, topic) => sum + Math.max(1, topic.tokens), 0);
3835
3929
  }
package/llms.txt CHANGED
@@ -374,7 +374,9 @@ The package intentionally separates durable base writes from expensive graph con
374
374
  `updateChat(messages, opts)`:
375
375
 
376
376
  - Registers its write synchronously, so a caller may intentionally fire-and-forget it and a following `getHistoryWindow` will still wait for that write.
377
- - Calculates tokens over `content`, `parts`, `payload`, and `metadata`, embeds the searchable text, and upserts raw messages by stable `messageId` before resolving.
377
+ - Calculates tokens over `content`, `parts`, `payload`, `contextPayload`, and `metadata`, embeds only the searchable text, and upserts raw messages by stable `messageId` before resolving.
378
+ - `contextPayload` is a replay-only host payload: it is stored and returned unchanged and counts toward the raw budget, but is excluded from embedding, FTS, Topic-summary input, and conversation graph extraction.
379
+ - Raw-message token usage always counts the complete stored `metadata`. When a raw batch is summarized, common tool-call/tool-result fields are removed only from the temporary LLM compression input; persisted metadata and uncompressed `recentMessages` remain unchanged.
378
380
  - Creates or updates the session record.
379
381
  - Updates the in-memory session cache.
380
382
  - Once `getHistoryWindow` has supplied the current model size, reaching the precompression threshold starts background compression.
@@ -392,7 +394,7 @@ The package intentionally separates durable base writes from expensive graph con
392
394
 
393
395
  - Lazily hydrates the session from persistent Topics plus raw messages after the newest Topic boundary.
394
396
  - Waits for registered message writes. If the hard raw budget is exceeded, it also waits for or starts compression until the returned context fits.
395
- - Returns `{ compressedContext, recentMessages, usage }`. `recentMessages` preserves the `RawMessage` input shape, including `messageId`, `parts`, `payload`, `metadata`, and `createdAt`.
397
+ - Returns `{ compressionRevision, compressedContext, recentMessages, usage }`. `recentMessages` preserves the `RawMessage` input shape, including `messageId`, `parts`, `payload`, `contextPayload`, `metadata`, and `createdAt`. `compressionRevision` changes when the returned Topic/raw compression boundary changes and is intended for host observability rather than optimistic locking.
396
398
  - The effective Topic budget is `min(compressedContextTokenLimit, usableContextTokens * compressedContextRatio)`; the remaining usable history budget is reserved for raw messages.
397
399
  - Cold hydration always restores all persisted Topics and never silently truncates them. Each rollup summarizes the oldest approximately half of the current Topic tokens; a single oversized Topic is re-summarized by itself.
398
400
  - A small Topic-only overage returns immediately and schedules a transient background rollup for the next read. It blocks only when the overage exceeds `topicCompactionSyncRatio` or Topic plus raw history cannot fit the usable window.
@@ -424,6 +426,7 @@ Use `wait: true` when the next line of code must immediately call `searchKnowled
424
426
  - `content: string` - required plain text used for embedding and retrieval.
425
427
  - `parts?: ContentPart[]` - optional multimodal content parts; stored serialized.
426
428
  - `payload?: unknown` - host-framework message payload stored and returned without interpretation.
429
+ - `contextPayload?: unknown` - replay-only host context stored and returned without interpretation. It counts toward the raw history budget but is excluded from retrieval, compression summaries, and graph extraction.
427
430
  - `usage?: number` - token count; estimated with `tiktoken` if omitted.
428
431
  - `metadata?: Record<string, unknown>` - custom metadata stored as JSON.
429
432
  - `createdAt?: number` - Unix milliseconds; default is current time.
@@ -617,8 +620,9 @@ Context:
617
620
 
618
621
  Returns the complete model-history window:
619
622
 
623
+ - `compressionRevision?: string` - lightweight identifier for the current Topic/raw compression boundary. Background or blocking compression changes it; ordinary raw appends are not guaranteed to do so.
620
624
  - `compressedContext: string` - chronological Topic summaries for the current model-size budget. A soft cold-start overage may be returned once while its transient rollup runs in the background.
621
- - `recentMessages: MemoryRawMessage[]` - all currently uncompressed raw messages in chronological order, preserving the host payload and metadata.
625
+ - `recentMessages: MemoryRawMessage[]` - all currently uncompressed raw messages in chronological order, preserving the host payload, replay-only context payload, and metadata.
622
626
  - `usage` - model size, usable history size, compressed usage, and dynamic raw-message budget/usage.
623
627
 
624
628
  Pass the active model's context size on every read. Omitting it uses `defaultModelContextTokens` (256K by default) and logs a warning.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppagent/memory",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "独立记忆系统模块,向量存储支持 LanceDB / SQLite(sqlite-vec) 双后端自动切换 + Grafeo 知识图谱",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",