@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 +10 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.js +100 -6
- package/llms.txt +7 -3
- package/package.json +1 -1
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
|
-
[
|
|
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
|
-
[
|
|
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) =>
|
|
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
|
|
2043
|
-
const
|
|
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
|
|
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.
|