@diegoaltoworks/chatter 0.59.0 → 0.60.0
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/channels/index.js +81 -0
- package/dist/channels/index.mjs +81 -0
- package/dist/channels/pipeline.d.ts +8 -0
- package/dist/channels/pipeline.d.ts.map +1 -1
- package/dist/channels/telegram/channel.d.ts +7 -0
- package/dist/channels/telegram/channel.d.ts.map +1 -1
- package/dist/channels/telegram/index.js +81 -0
- package/dist/channels/telegram/index.mjs +81 -0
- package/dist/channels/telegram/webhook.d.ts +7 -0
- package/dist/channels/telegram/webhook.d.ts.map +1 -1
- package/dist/channels/whatsapp/inbound.d.ts +7 -0
- package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.js +81 -0
- package/dist/channels/whatsapp/index.mjs +81 -0
- package/dist/history/compaction.d.ts +61 -0
- package/dist/history/compaction.d.ts.map +1 -0
- package/dist/history/index.d.ts +7 -0
- package/dist/history/index.d.ts.map +1 -1
- package/dist/history/index.js +143 -0
- package/dist/history/index.mjs +141 -0
- package/package.json +1 -1
package/dist/channels/index.js
CHANGED
|
@@ -262,6 +262,75 @@ ${ctx.join("\n\n")}`
|
|
|
262
262
|
return { system, messages, context: ctx };
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// src/history/compaction.ts
|
|
266
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
267
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
268
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
269
|
+
function defaultSummarize(client, model) {
|
|
270
|
+
return async ({ turns }) => {
|
|
271
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
272
|
+
const { content } = await answerOnce({
|
|
273
|
+
client,
|
|
274
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
275
|
+
messages: [{ role: "user", content: transcript }],
|
|
276
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
277
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
278
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
279
|
+
mode: "public",
|
|
280
|
+
model
|
|
281
|
+
});
|
|
282
|
+
return content;
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function createHistoryCompactor(deps, options) {
|
|
286
|
+
const { threshold, keep, logger } = options;
|
|
287
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
288
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
289
|
+
}
|
|
290
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
291
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
292
|
+
}
|
|
293
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
294
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
295
|
+
return {
|
|
296
|
+
async maybeCompact(store, conversationId) {
|
|
297
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
298
|
+
if (turns.length < threshold) return;
|
|
299
|
+
const older = turns.slice(0, turns.length - keep);
|
|
300
|
+
let summary;
|
|
301
|
+
let timer;
|
|
302
|
+
try {
|
|
303
|
+
summary = await Promise.race([
|
|
304
|
+
summarize({ conversationId, turns: older }),
|
|
305
|
+
new Promise((_, reject) => {
|
|
306
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
307
|
+
})
|
|
308
|
+
]);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
logger?.error(
|
|
311
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
312
|
+
error
|
|
313
|
+
);
|
|
314
|
+
return;
|
|
315
|
+
} finally {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
}
|
|
318
|
+
const trimmed = summary.trim();
|
|
319
|
+
if (!trimmed) return;
|
|
320
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
321
|
+
const recent = fresh.slice(older.length);
|
|
322
|
+
await store.clear(conversationId);
|
|
323
|
+
await store.append(conversationId, {
|
|
324
|
+
role: "assistant",
|
|
325
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
326
|
+
});
|
|
327
|
+
for (const turn of recent) {
|
|
328
|
+
await store.append(conversationId, turn);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
265
334
|
// src/channels/pipeline.ts
|
|
266
335
|
var DEFAULT_HISTORY_LIMIT = 20;
|
|
267
336
|
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
@@ -292,6 +361,10 @@ function createInboundPipeline(deps, config) {
|
|
|
292
361
|
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
293
362
|
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
294
363
|
const mode = "public";
|
|
364
|
+
const compactor = config.history?.compaction ? createHistoryCompactor(
|
|
365
|
+
{ client: deps.client },
|
|
366
|
+
{ model: config.model, ...config.history.compaction }
|
|
367
|
+
) : void 0;
|
|
295
368
|
return async function handleInboundMessage(msg, turn) {
|
|
296
369
|
const allowedChats = config.allowedChats ?? [];
|
|
297
370
|
const action = decideChannelAction(msg, {
|
|
@@ -367,6 +440,14 @@ function createInboundPipeline(deps, config) {
|
|
|
367
440
|
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
368
441
|
}
|
|
369
442
|
await turn.reply.sendAnswer(msg.chatId, content);
|
|
443
|
+
const history = config.history;
|
|
444
|
+
if (compactor && historyEnabled && history) {
|
|
445
|
+
try {
|
|
446
|
+
await compactor.maybeCompact(history.store, conversationId);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
config.logger?.error(`history compaction failed for "${conversationId}"`, error);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
370
451
|
return { action: "reply", content };
|
|
371
452
|
};
|
|
372
453
|
}
|
package/dist/channels/index.mjs
CHANGED
|
@@ -229,6 +229,75 @@ ${ctx.join("\n\n")}`
|
|
|
229
229
|
return { system, messages, context: ctx };
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// src/history/compaction.ts
|
|
233
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
234
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
235
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
236
|
+
function defaultSummarize(client, model) {
|
|
237
|
+
return async ({ turns }) => {
|
|
238
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
239
|
+
const { content } = await answerOnce({
|
|
240
|
+
client,
|
|
241
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
242
|
+
messages: [{ role: "user", content: transcript }],
|
|
243
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
244
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
245
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
246
|
+
mode: "public",
|
|
247
|
+
model
|
|
248
|
+
});
|
|
249
|
+
return content;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function createHistoryCompactor(deps, options) {
|
|
253
|
+
const { threshold, keep, logger } = options;
|
|
254
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
255
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
256
|
+
}
|
|
257
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
258
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
259
|
+
}
|
|
260
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
261
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
262
|
+
return {
|
|
263
|
+
async maybeCompact(store, conversationId) {
|
|
264
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
265
|
+
if (turns.length < threshold) return;
|
|
266
|
+
const older = turns.slice(0, turns.length - keep);
|
|
267
|
+
let summary;
|
|
268
|
+
let timer;
|
|
269
|
+
try {
|
|
270
|
+
summary = await Promise.race([
|
|
271
|
+
summarize({ conversationId, turns: older }),
|
|
272
|
+
new Promise((_, reject) => {
|
|
273
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
274
|
+
})
|
|
275
|
+
]);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
logger?.error(
|
|
278
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
279
|
+
error
|
|
280
|
+
);
|
|
281
|
+
return;
|
|
282
|
+
} finally {
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
}
|
|
285
|
+
const trimmed = summary.trim();
|
|
286
|
+
if (!trimmed) return;
|
|
287
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
288
|
+
const recent = fresh.slice(older.length);
|
|
289
|
+
await store.clear(conversationId);
|
|
290
|
+
await store.append(conversationId, {
|
|
291
|
+
role: "assistant",
|
|
292
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
293
|
+
});
|
|
294
|
+
for (const turn of recent) {
|
|
295
|
+
await store.append(conversationId, turn);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
232
301
|
// src/channels/pipeline.ts
|
|
233
302
|
var DEFAULT_HISTORY_LIMIT = 20;
|
|
234
303
|
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
@@ -259,6 +328,10 @@ function createInboundPipeline(deps, config) {
|
|
|
259
328
|
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
260
329
|
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
261
330
|
const mode = "public";
|
|
331
|
+
const compactor = config.history?.compaction ? createHistoryCompactor(
|
|
332
|
+
{ client: deps.client },
|
|
333
|
+
{ model: config.model, ...config.history.compaction }
|
|
334
|
+
) : void 0;
|
|
262
335
|
return async function handleInboundMessage(msg, turn) {
|
|
263
336
|
const allowedChats = config.allowedChats ?? [];
|
|
264
337
|
const action = decideChannelAction(msg, {
|
|
@@ -334,6 +407,14 @@ function createInboundPipeline(deps, config) {
|
|
|
334
407
|
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
335
408
|
}
|
|
336
409
|
await turn.reply.sendAnswer(msg.chatId, content);
|
|
410
|
+
const history = config.history;
|
|
411
|
+
if (compactor && historyEnabled && history) {
|
|
412
|
+
try {
|
|
413
|
+
await compactor.maybeCompact(history.store, conversationId);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
config.logger?.error(`history compaction failed for "${conversationId}"`, error);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
337
418
|
return { action: "reply", content };
|
|
338
419
|
};
|
|
339
420
|
}
|
|
@@ -20,6 +20,7 @@ import type { Logger } from "../core/logger";
|
|
|
20
20
|
import { type RerankContext, type RewriteQuery } from "../core/pipeline";
|
|
21
21
|
import type { PromptLoader } from "../core/prompts";
|
|
22
22
|
import type { VectorStore } from "../core/retrieval";
|
|
23
|
+
import type { HistoryCompactionOptions } from "../history/compaction";
|
|
23
24
|
import type { HistoryStore } from "../history/types";
|
|
24
25
|
import { type ChannelMessage } from "./gates";
|
|
25
26
|
/** Where a pipeline turn's output goes. Distinct from `ChannelSender` (`./senders`) — a transport implementing this may thread/quote the chat answer without doing the same for a plain gate acknowledgement. */
|
|
@@ -69,6 +70,13 @@ export interface InboundPipelineConfig {
|
|
|
69
70
|
* opted out. @default every sender is enabled
|
|
70
71
|
*/
|
|
71
72
|
historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
|
|
73
|
+
/**
|
|
74
|
+
* Summarize-then-truncate compaction, checked right after each reply is
|
|
75
|
+
* appended. Off by default — a conversation's window grows unbounded
|
|
76
|
+
* (aside from the store's own `maxPerConversation`) until this is set.
|
|
77
|
+
* See `../history/compaction`.
|
|
78
|
+
*/
|
|
79
|
+
compaction?: HistoryCompactionOptions;
|
|
72
80
|
};
|
|
73
81
|
/** Group chats eligible for a reply. Empty (default) = every group. Has no effect on DMs, which always reply. */
|
|
74
82
|
allowedChats?: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/channels/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EACL,KAAK,cAAc,EAIpB,MAAM,SAAS,CAAC;AAEjB,iNAAiN;AACjN,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,kFAAkF;IAClF,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,0GAA0G;IAC1G,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,iHAAiH;IACjH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,kGAAkG;IAClG,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sGAAsG;IACtG,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,gFAAgF;IAChF,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf;;;;;;WAMG;QACH,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/channels/pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -24,6 +24,7 @@ import type { AnswerFn, TransformReply } from "../../core/answer";
|
|
|
24
24
|
import type { BucketsFor } from "../../core/buckets";
|
|
25
25
|
import { type Logger } from "../../core/logger";
|
|
26
26
|
import type { RerankContext, RewriteQuery } from "../../core/pipeline";
|
|
27
|
+
import type { HistoryCompactionOptions } from "../../history/compaction";
|
|
27
28
|
import type { HistoryStore } from "../../history/types";
|
|
28
29
|
import type { Channel } from "../index";
|
|
29
30
|
import { type TelegramApi } from "./api";
|
|
@@ -61,6 +62,12 @@ export interface TelegramChannelConfig {
|
|
|
61
62
|
* @default every sender is enabled
|
|
62
63
|
*/
|
|
63
64
|
historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Summarize-then-truncate compaction — see
|
|
67
|
+
* `InboundPipelineConfig.history.compaction` in `./channels`.
|
|
68
|
+
* @default off
|
|
69
|
+
*/
|
|
70
|
+
compaction?: HistoryCompactionOptions;
|
|
64
71
|
};
|
|
65
72
|
muteRegex?: RegExp;
|
|
66
73
|
unmuteRegex?: RegExp;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC;AAQ5D,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB,8GAA8G;IAC9G,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,0IAA0I;IAC1I,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,iJAAiJ;IACjJ,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,kIAAkI;IAClI,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kIAAkI;IAClI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4GAA4G;IAC5G,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,kFAAkF;IAClF,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf;;;;WAIG;QACH,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/channel.ts"],"names":[],"mappings":"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"}
|
|
@@ -393,6 +393,75 @@ ${ctx.join("\n\n")}`
|
|
|
393
393
|
return { system, messages, context: ctx };
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
// src/history/compaction.ts
|
|
397
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
398
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
399
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
400
|
+
function defaultSummarize(client, model) {
|
|
401
|
+
return async ({ turns }) => {
|
|
402
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
403
|
+
const { content } = await answerOnce({
|
|
404
|
+
client,
|
|
405
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
406
|
+
messages: [{ role: "user", content: transcript }],
|
|
407
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
408
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
409
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
410
|
+
mode: "public",
|
|
411
|
+
model
|
|
412
|
+
});
|
|
413
|
+
return content;
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function createHistoryCompactor(deps, options) {
|
|
417
|
+
const { threshold, keep, logger } = options;
|
|
418
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
419
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
420
|
+
}
|
|
421
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
422
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
423
|
+
}
|
|
424
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
425
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
426
|
+
return {
|
|
427
|
+
async maybeCompact(store, conversationId) {
|
|
428
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
429
|
+
if (turns.length < threshold) return;
|
|
430
|
+
const older = turns.slice(0, turns.length - keep);
|
|
431
|
+
let summary;
|
|
432
|
+
let timer;
|
|
433
|
+
try {
|
|
434
|
+
summary = await Promise.race([
|
|
435
|
+
summarize({ conversationId, turns: older }),
|
|
436
|
+
new Promise((_, reject) => {
|
|
437
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
438
|
+
})
|
|
439
|
+
]);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
logger?.error(
|
|
442
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
443
|
+
error
|
|
444
|
+
);
|
|
445
|
+
return;
|
|
446
|
+
} finally {
|
|
447
|
+
clearTimeout(timer);
|
|
448
|
+
}
|
|
449
|
+
const trimmed = summary.trim();
|
|
450
|
+
if (!trimmed) return;
|
|
451
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
452
|
+
const recent = fresh.slice(older.length);
|
|
453
|
+
await store.clear(conversationId);
|
|
454
|
+
await store.append(conversationId, {
|
|
455
|
+
role: "assistant",
|
|
456
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
457
|
+
});
|
|
458
|
+
for (const turn of recent) {
|
|
459
|
+
await store.append(conversationId, turn);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
396
465
|
// src/channels/gates.ts
|
|
397
466
|
function matches(re, text) {
|
|
398
467
|
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
@@ -473,6 +542,10 @@ function createInboundPipeline(deps, config) {
|
|
|
473
542
|
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
474
543
|
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
475
544
|
const mode = "public";
|
|
545
|
+
const compactor = config.history?.compaction ? createHistoryCompactor(
|
|
546
|
+
{ client: deps.client },
|
|
547
|
+
{ model: config.model, ...config.history.compaction }
|
|
548
|
+
) : void 0;
|
|
476
549
|
return async function handleInboundMessage(msg, turn) {
|
|
477
550
|
const allowedChats = config.allowedChats ?? [];
|
|
478
551
|
const action = decideChannelAction(msg, {
|
|
@@ -548,6 +621,14 @@ function createInboundPipeline(deps, config) {
|
|
|
548
621
|
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
549
622
|
}
|
|
550
623
|
await turn.reply.sendAnswer(msg.chatId, content);
|
|
624
|
+
const history = config.history;
|
|
625
|
+
if (compactor && historyEnabled && history) {
|
|
626
|
+
try {
|
|
627
|
+
await compactor.maybeCompact(history.store, conversationId);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
config.logger?.error(`history compaction failed for "${conversationId}"`, error);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
551
632
|
return { action: "reply", content };
|
|
552
633
|
};
|
|
553
634
|
}
|
|
@@ -348,6 +348,75 @@ ${ctx.join("\n\n")}`
|
|
|
348
348
|
return { system, messages, context: ctx };
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
// src/history/compaction.ts
|
|
352
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
353
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
354
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
355
|
+
function defaultSummarize(client, model) {
|
|
356
|
+
return async ({ turns }) => {
|
|
357
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
358
|
+
const { content } = await answerOnce({
|
|
359
|
+
client,
|
|
360
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
361
|
+
messages: [{ role: "user", content: transcript }],
|
|
362
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
363
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
364
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
365
|
+
mode: "public",
|
|
366
|
+
model
|
|
367
|
+
});
|
|
368
|
+
return content;
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function createHistoryCompactor(deps, options) {
|
|
372
|
+
const { threshold, keep, logger } = options;
|
|
373
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
374
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
375
|
+
}
|
|
376
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
377
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
378
|
+
}
|
|
379
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
380
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
381
|
+
return {
|
|
382
|
+
async maybeCompact(store, conversationId) {
|
|
383
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
384
|
+
if (turns.length < threshold) return;
|
|
385
|
+
const older = turns.slice(0, turns.length - keep);
|
|
386
|
+
let summary;
|
|
387
|
+
let timer;
|
|
388
|
+
try {
|
|
389
|
+
summary = await Promise.race([
|
|
390
|
+
summarize({ conversationId, turns: older }),
|
|
391
|
+
new Promise((_, reject) => {
|
|
392
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
393
|
+
})
|
|
394
|
+
]);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
logger?.error(
|
|
397
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
398
|
+
error
|
|
399
|
+
);
|
|
400
|
+
return;
|
|
401
|
+
} finally {
|
|
402
|
+
clearTimeout(timer);
|
|
403
|
+
}
|
|
404
|
+
const trimmed = summary.trim();
|
|
405
|
+
if (!trimmed) return;
|
|
406
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
407
|
+
const recent = fresh.slice(older.length);
|
|
408
|
+
await store.clear(conversationId);
|
|
409
|
+
await store.append(conversationId, {
|
|
410
|
+
role: "assistant",
|
|
411
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
412
|
+
});
|
|
413
|
+
for (const turn of recent) {
|
|
414
|
+
await store.append(conversationId, turn);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
351
420
|
// src/channels/gates.ts
|
|
352
421
|
function matches(re, text) {
|
|
353
422
|
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
@@ -428,6 +497,10 @@ function createInboundPipeline(deps, config) {
|
|
|
428
497
|
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
429
498
|
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
430
499
|
const mode = "public";
|
|
500
|
+
const compactor = config.history?.compaction ? createHistoryCompactor(
|
|
501
|
+
{ client: deps.client },
|
|
502
|
+
{ model: config.model, ...config.history.compaction }
|
|
503
|
+
) : void 0;
|
|
431
504
|
return async function handleInboundMessage(msg, turn) {
|
|
432
505
|
const allowedChats = config.allowedChats ?? [];
|
|
433
506
|
const action = decideChannelAction(msg, {
|
|
@@ -503,6 +576,14 @@ function createInboundPipeline(deps, config) {
|
|
|
503
576
|
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
504
577
|
}
|
|
505
578
|
await turn.reply.sendAnswer(msg.chatId, content);
|
|
579
|
+
const history = config.history;
|
|
580
|
+
if (compactor && historyEnabled && history) {
|
|
581
|
+
try {
|
|
582
|
+
await compactor.maybeCompact(history.store, conversationId);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
config.logger?.error(`history compaction failed for "${conversationId}"`, error);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
506
587
|
return { action: "reply", content };
|
|
507
588
|
};
|
|
508
589
|
}
|
|
@@ -23,6 +23,7 @@ import type { AnswerFn, TransformReply } from "../../core/answer";
|
|
|
23
23
|
import type { BucketsFor } from "../../core/buckets";
|
|
24
24
|
import { type Logger } from "../../core/logger";
|
|
25
25
|
import type { RerankContext, RewriteQuery } from "../../core/pipeline";
|
|
26
|
+
import type { HistoryCompactionOptions } from "../../history/compaction";
|
|
26
27
|
import type { HistoryStore } from "../../history/types";
|
|
27
28
|
import type { CustomRoutes } from "../../types";
|
|
28
29
|
import { type TelegramApi } from "./api";
|
|
@@ -62,6 +63,12 @@ export interface TelegramWebhookConfig {
|
|
|
62
63
|
/** Most recent turns to load per reply. @default 20 */
|
|
63
64
|
limit?: number;
|
|
64
65
|
historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* Summarize-then-truncate compaction — see
|
|
68
|
+
* `InboundPipelineConfig.history.compaction` in `./channels`.
|
|
69
|
+
* @default off
|
|
70
|
+
*/
|
|
71
|
+
compaction?: HistoryCompactionOptions;
|
|
65
72
|
};
|
|
66
73
|
muteRegex?: RegExp;
|
|
67
74
|
unmuteRegex?: RegExp;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAqB,KAAK,WAAW,EAAuB,MAAM,OAAO,CAAC;AAMjF,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8GAA8G;IAC9G,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kIAAkI;IAClI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4GAA4G;IAC5G,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,kFAAkF;IAClF,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAqB,KAAK,WAAW,EAAuB,MAAM,OAAO,CAAC;AAMjF,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8GAA8G;IAC9G,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kIAAkI;IAClI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4GAA4G;IAC5G,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,kFAAkF;IAClF,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QACnE;;;;WAIG;QACH,UAAU,CAAC,EAAE,wBAAwB,CAAC;KACvC,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mGAAmG;IACnG,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qJAAqJ;IACrJ,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,qBAAqB,GAAG,YAAY,CAkHtF"}
|
|
@@ -54,6 +54,7 @@ import { type Logger } from "../../core/logger";
|
|
|
54
54
|
import type { RerankContext, RewriteQuery } from "../../core/pipeline";
|
|
55
55
|
import type { PromptLoader } from "../../core/prompts";
|
|
56
56
|
import type { VectorStore } from "../../core/retrieval";
|
|
57
|
+
import type { HistoryCompactionOptions } from "../../history/compaction";
|
|
57
58
|
import type { HistoryStore } from "../../history/types";
|
|
58
59
|
import { type ChannelMessage, type SessionIdentityRegistry } from "../gates";
|
|
59
60
|
import type { WhatsAppMessageEvent } from "./channel";
|
|
@@ -199,6 +200,12 @@ export interface WhatsAppInboundConfig {
|
|
|
199
200
|
* @default every sender is enabled
|
|
200
201
|
*/
|
|
201
202
|
historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
|
|
203
|
+
/**
|
|
204
|
+
* Summarize-then-truncate compaction — see
|
|
205
|
+
* `InboundPipelineConfig.history.compaction` in `./channels`.
|
|
206
|
+
* @default off
|
|
207
|
+
*/
|
|
208
|
+
compaction?: HistoryCompactionOptions;
|
|
202
209
|
};
|
|
203
210
|
now?: () => number;
|
|
204
211
|
/** Logger for gate/dispatch diagnostics. Default: a console logger writing to stderr. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/inbound.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/inbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -840,6 +840,75 @@ ${ctx.join("\n\n")}`
|
|
|
840
840
|
return { system, messages, context: ctx };
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
// src/history/compaction.ts
|
|
844
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
845
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
846
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
847
|
+
function defaultSummarize(client, model) {
|
|
848
|
+
return async ({ turns }) => {
|
|
849
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
850
|
+
const { content } = await answerOnce({
|
|
851
|
+
client,
|
|
852
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
853
|
+
messages: [{ role: "user", content: transcript }],
|
|
854
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
855
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
856
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
857
|
+
mode: "public",
|
|
858
|
+
model
|
|
859
|
+
});
|
|
860
|
+
return content;
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function createHistoryCompactor(deps, options) {
|
|
864
|
+
const { threshold, keep, logger } = options;
|
|
865
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
866
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
867
|
+
}
|
|
868
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
869
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
870
|
+
}
|
|
871
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
872
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
873
|
+
return {
|
|
874
|
+
async maybeCompact(store, conversationId) {
|
|
875
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
876
|
+
if (turns.length < threshold) return;
|
|
877
|
+
const older = turns.slice(0, turns.length - keep);
|
|
878
|
+
let summary;
|
|
879
|
+
let timer;
|
|
880
|
+
try {
|
|
881
|
+
summary = await Promise.race([
|
|
882
|
+
summarize({ conversationId, turns: older }),
|
|
883
|
+
new Promise((_, reject) => {
|
|
884
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
885
|
+
})
|
|
886
|
+
]);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
logger?.error(
|
|
889
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
890
|
+
error
|
|
891
|
+
);
|
|
892
|
+
return;
|
|
893
|
+
} finally {
|
|
894
|
+
clearTimeout(timer);
|
|
895
|
+
}
|
|
896
|
+
const trimmed = summary.trim();
|
|
897
|
+
if (!trimmed) return;
|
|
898
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
899
|
+
const recent = fresh.slice(older.length);
|
|
900
|
+
await store.clear(conversationId);
|
|
901
|
+
await store.append(conversationId, {
|
|
902
|
+
role: "assistant",
|
|
903
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
904
|
+
});
|
|
905
|
+
for (const turn of recent) {
|
|
906
|
+
await store.append(conversationId, turn);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
843
912
|
// src/channels/pipeline.ts
|
|
844
913
|
var DEFAULT_HISTORY_LIMIT = 20;
|
|
845
914
|
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
@@ -870,6 +939,10 @@ function createInboundPipeline(deps, config) {
|
|
|
870
939
|
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
871
940
|
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
872
941
|
const mode = "public";
|
|
942
|
+
const compactor = config.history?.compaction ? createHistoryCompactor(
|
|
943
|
+
{ client: deps.client },
|
|
944
|
+
{ model: config.model, ...config.history.compaction }
|
|
945
|
+
) : void 0;
|
|
873
946
|
return async function handleInboundMessage(msg, turn) {
|
|
874
947
|
const allowedChats = config.allowedChats ?? [];
|
|
875
948
|
const action = decideChannelAction(msg, {
|
|
@@ -945,6 +1018,14 @@ function createInboundPipeline(deps, config) {
|
|
|
945
1018
|
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
946
1019
|
}
|
|
947
1020
|
await turn.reply.sendAnswer(msg.chatId, content);
|
|
1021
|
+
const history = config.history;
|
|
1022
|
+
if (compactor && historyEnabled && history) {
|
|
1023
|
+
try {
|
|
1024
|
+
await compactor.maybeCompact(history.store, conversationId);
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
config.logger?.error(`history compaction failed for "${conversationId}"`, error);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
948
1029
|
return { action: "reply", content };
|
|
949
1030
|
};
|
|
950
1031
|
}
|
|
@@ -778,6 +778,75 @@ ${ctx.join("\n\n")}`
|
|
|
778
778
|
return { system, messages, context: ctx };
|
|
779
779
|
}
|
|
780
780
|
|
|
781
|
+
// src/history/compaction.ts
|
|
782
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
783
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
784
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
785
|
+
function defaultSummarize(client, model) {
|
|
786
|
+
return async ({ turns }) => {
|
|
787
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
788
|
+
const { content } = await answerOnce({
|
|
789
|
+
client,
|
|
790
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
791
|
+
messages: [{ role: "user", content: transcript }],
|
|
792
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
793
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
794
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
795
|
+
mode: "public",
|
|
796
|
+
model
|
|
797
|
+
});
|
|
798
|
+
return content;
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function createHistoryCompactor(deps, options) {
|
|
802
|
+
const { threshold, keep, logger } = options;
|
|
803
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
804
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
805
|
+
}
|
|
806
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
807
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
808
|
+
}
|
|
809
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
810
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
811
|
+
return {
|
|
812
|
+
async maybeCompact(store, conversationId) {
|
|
813
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
814
|
+
if (turns.length < threshold) return;
|
|
815
|
+
const older = turns.slice(0, turns.length - keep);
|
|
816
|
+
let summary;
|
|
817
|
+
let timer;
|
|
818
|
+
try {
|
|
819
|
+
summary = await Promise.race([
|
|
820
|
+
summarize({ conversationId, turns: older }),
|
|
821
|
+
new Promise((_, reject) => {
|
|
822
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
823
|
+
})
|
|
824
|
+
]);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
logger?.error(
|
|
827
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
828
|
+
error
|
|
829
|
+
);
|
|
830
|
+
return;
|
|
831
|
+
} finally {
|
|
832
|
+
clearTimeout(timer);
|
|
833
|
+
}
|
|
834
|
+
const trimmed = summary.trim();
|
|
835
|
+
if (!trimmed) return;
|
|
836
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
837
|
+
const recent = fresh.slice(older.length);
|
|
838
|
+
await store.clear(conversationId);
|
|
839
|
+
await store.append(conversationId, {
|
|
840
|
+
role: "assistant",
|
|
841
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
842
|
+
});
|
|
843
|
+
for (const turn of recent) {
|
|
844
|
+
await store.append(conversationId, turn);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
781
850
|
// src/channels/pipeline.ts
|
|
782
851
|
var DEFAULT_HISTORY_LIMIT = 20;
|
|
783
852
|
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
@@ -808,6 +877,10 @@ function createInboundPipeline(deps, config) {
|
|
|
808
877
|
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
809
878
|
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
810
879
|
const mode = "public";
|
|
880
|
+
const compactor = config.history?.compaction ? createHistoryCompactor(
|
|
881
|
+
{ client: deps.client },
|
|
882
|
+
{ model: config.model, ...config.history.compaction }
|
|
883
|
+
) : void 0;
|
|
811
884
|
return async function handleInboundMessage(msg, turn) {
|
|
812
885
|
const allowedChats = config.allowedChats ?? [];
|
|
813
886
|
const action = decideChannelAction(msg, {
|
|
@@ -883,6 +956,14 @@ function createInboundPipeline(deps, config) {
|
|
|
883
956
|
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
884
957
|
}
|
|
885
958
|
await turn.reply.sendAnswer(msg.chatId, content);
|
|
959
|
+
const history = config.history;
|
|
960
|
+
if (compactor && historyEnabled && history) {
|
|
961
|
+
try {
|
|
962
|
+
await compactor.maybeCompact(history.store, conversationId);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
config.logger?.error(`history compaction failed for "${conversationId}"`, error);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
886
967
|
return { action: "reply", content };
|
|
887
968
|
};
|
|
888
969
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarize-then-truncate compaction for `HistoryStore` — folds older turns
|
|
3
|
+
* into a single stored summary row once a conversation's window grows past a
|
|
4
|
+
* threshold, so a long-running conversation's prompt doesn't grow without
|
|
5
|
+
* bound. Off by default; a caller wires `maybeCompact` in right after its own
|
|
6
|
+
* `store.append` call, the same way `./index`'s docs describe wiring the
|
|
7
|
+
* store itself.
|
|
8
|
+
*
|
|
9
|
+
* The summarize step races a timeout the same way `../images/captionComposer`
|
|
10
|
+
* races its compose step, except the safe fallback here is "leave history
|
|
11
|
+
* untouched" rather than a template pool — a stuck or failing summarize call
|
|
12
|
+
* must never lose turns nobody got to read back.
|
|
13
|
+
*/
|
|
14
|
+
import type OpenAI from "openai";
|
|
15
|
+
import type { Logger } from "../core/logger";
|
|
16
|
+
import type { HistoryMessage, HistoryStore } from "./types";
|
|
17
|
+
/** What a summarize step receives: the turns being folded away, oldest first. Never includes the turns `keep` retains verbatim. */
|
|
18
|
+
export interface SummarizeRequest {
|
|
19
|
+
conversationId: string;
|
|
20
|
+
turns: HistoryMessage[];
|
|
21
|
+
}
|
|
22
|
+
/** Best-effort compaction step, typically an LLM call. */
|
|
23
|
+
export type SummarizeFn = (request: SummarizeRequest) => Promise<string>;
|
|
24
|
+
export interface HistoryCompactionOptions {
|
|
25
|
+
/** Turn count that triggers compaction. */
|
|
26
|
+
threshold: number;
|
|
27
|
+
/** Most recent turns kept verbatim after compaction; everything older is folded into the summary. Must be less than `threshold`. */
|
|
28
|
+
keep: number;
|
|
29
|
+
/**
|
|
30
|
+
* Best-effort summarize step. Defaults to `answerOnce` with a neutral
|
|
31
|
+
* prompt and no `answerFn` (so it always uses the built-in completion,
|
|
32
|
+
* never a caller's brain hook) — set this to route summarization through a
|
|
33
|
+
* caller's own `answerFn`, a cheaper model, or a non-LLM summarizer.
|
|
34
|
+
*/
|
|
35
|
+
summarize?: SummarizeFn;
|
|
36
|
+
/** Model for the default `summarize` step. Ignored when `summarize` is supplied. Falls back to `answerOnce`'s own default model when unset. */
|
|
37
|
+
model?: string;
|
|
38
|
+
/** Milliseconds before `summarize` is abandoned in favour of leaving history untouched for this turn. @default 8000 */
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
logger?: Pick<Logger, "error">;
|
|
41
|
+
}
|
|
42
|
+
export interface HistoryCompactor {
|
|
43
|
+
/**
|
|
44
|
+
* Compacts `conversationId` if its stored turn count has reached
|
|
45
|
+
* `threshold`. A no-op below threshold. A failed or timed-out `summarize`
|
|
46
|
+
* leaves the store exactly as it was and does not throw; a failure in the
|
|
47
|
+
* store itself (`load`/`clear`/`append`) propagates like any other store
|
|
48
|
+
* failure.
|
|
49
|
+
*/
|
|
50
|
+
maybeCompact(store: HistoryStore, conversationId: string): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/** Marks a stored row as a compaction summary rather than a real turn — informational only, nothing parses it back out. */
|
|
53
|
+
export declare const SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
54
|
+
/**
|
|
55
|
+
* `deps.client` builds the default `summarize` step; unused when a caller
|
|
56
|
+
* supplies its own.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createHistoryCompactor(deps: {
|
|
59
|
+
client: OpenAI;
|
|
60
|
+
}, options: HistoryCompactionOptions): HistoryCompactor;
|
|
61
|
+
//# sourceMappingURL=compaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/history/compaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5D,mIAAmI;AACnI,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,0DAA0D;AAC1D,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,wBAAwB;IACvC,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,oIAAoI;IACpI,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,+IAA+I;IAC/I,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uHAAuH;IACvH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAWD,2HAA2H;AAC3H,eAAO,MAAM,cAAc,sCAAsC,CAAC;AAoBlE;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACxB,OAAO,EAAE,wBAAwB,GAChC,gBAAgB,CAgElB"}
|
package/dist/history/index.d.ts
CHANGED
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
* implement it. The shipped Turso implementation needs `@libsql/client`,
|
|
11
11
|
* which chatter already expects for retrieval.
|
|
12
12
|
*
|
|
13
|
+
* `./compaction` is an optional layer on top: summarize-then-truncate for a
|
|
14
|
+
* conversation whose window has grown past a threshold, built entirely on
|
|
15
|
+
* the store's own `load`/`clear`/`append` so it works with any
|
|
16
|
+
* `HistoryStore`, not just the Turso one.
|
|
17
|
+
*
|
|
13
18
|
* @packageDocumentation
|
|
14
19
|
*/
|
|
20
|
+
export type { HistoryCompactionOptions, HistoryCompactor, SummarizeFn, SummarizeRequest, } from "./compaction";
|
|
21
|
+
export { createHistoryCompactor, SUMMARY_PREFIX } from "./compaction";
|
|
15
22
|
export type { TursoHistoryStoreOptions } from "./tursoStore";
|
|
16
23
|
export { createTursoHistoryStore } from "./tursoStore";
|
|
17
24
|
export type { HistoryMessage, HistoryStore } from "./types";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,YAAY,EACV,wBAAwB,EACxB,gBAAgB,EAChB,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACtE,YAAY,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/history/index.js
CHANGED
|
@@ -20,10 +20,151 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/history/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
SUMMARY_PREFIX: () => SUMMARY_PREFIX,
|
|
24
|
+
createHistoryCompactor: () => createHistoryCompactor,
|
|
23
25
|
createTursoHistoryStore: () => createTursoHistoryStore
|
|
24
26
|
});
|
|
25
27
|
module.exports = __toCommonJS(index_exports);
|
|
26
28
|
|
|
29
|
+
// src/core/guardrails.ts
|
|
30
|
+
function detectLeakage(text) {
|
|
31
|
+
const flags = [
|
|
32
|
+
/system prompt/i,
|
|
33
|
+
/hidden instruction/i,
|
|
34
|
+
/here (are|is) (my|the) rules/i,
|
|
35
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
36
|
+
/tool instructions/i
|
|
37
|
+
];
|
|
38
|
+
return flags.some((rx) => rx.test(text));
|
|
39
|
+
}
|
|
40
|
+
function scrubOutput(text) {
|
|
41
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
42
|
+
}
|
|
43
|
+
function guardOutput(text) {
|
|
44
|
+
if (detectLeakage(text)) {
|
|
45
|
+
return "Sorry, I can't share internal instructions. How else can I help?";
|
|
46
|
+
}
|
|
47
|
+
return scrubOutput(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/core/llm.ts
|
|
51
|
+
var DEFAULT_MODEL = "gpt-4o";
|
|
52
|
+
async function completeOnce({
|
|
53
|
+
client,
|
|
54
|
+
system,
|
|
55
|
+
messages,
|
|
56
|
+
temperature = 0.2,
|
|
57
|
+
model = DEFAULT_MODEL
|
|
58
|
+
}) {
|
|
59
|
+
const res = await client.chat.completions.create({
|
|
60
|
+
model,
|
|
61
|
+
temperature,
|
|
62
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
63
|
+
});
|
|
64
|
+
const text = res.choices[0]?.message?.content ?? "";
|
|
65
|
+
return {
|
|
66
|
+
content: guardOutput(text),
|
|
67
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/core/answer.ts
|
|
72
|
+
var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
|
|
73
|
+
async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
|
|
74
|
+
const result = await answerFn({ system, messages, mode, sender, conversationId });
|
|
75
|
+
const raw = typeof result === "string" ? result : result?.content;
|
|
76
|
+
const usage = typeof result === "string" ? void 0 : result?.usage;
|
|
77
|
+
return {
|
|
78
|
+
content: guardOutput(typeof raw === "string" ? raw : ""),
|
|
79
|
+
usage: usage ?? noUsage()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function answerOnce({
|
|
83
|
+
answerFn,
|
|
84
|
+
client,
|
|
85
|
+
system,
|
|
86
|
+
messages,
|
|
87
|
+
mode,
|
|
88
|
+
sender,
|
|
89
|
+
conversationId,
|
|
90
|
+
temperature,
|
|
91
|
+
model
|
|
92
|
+
}) {
|
|
93
|
+
if (answerFn) {
|
|
94
|
+
return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
|
|
95
|
+
}
|
|
96
|
+
return completeOnce({ client, system, messages, temperature, model });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/history/compaction.ts
|
|
100
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
101
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
102
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
103
|
+
function defaultSummarize(client, model) {
|
|
104
|
+
return async ({ turns }) => {
|
|
105
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
106
|
+
const { content } = await answerOnce({
|
|
107
|
+
client,
|
|
108
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
109
|
+
messages: [{ role: "user", content: transcript }],
|
|
110
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
111
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
112
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
113
|
+
mode: "public",
|
|
114
|
+
model
|
|
115
|
+
});
|
|
116
|
+
return content;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function createHistoryCompactor(deps, options) {
|
|
120
|
+
const { threshold, keep, logger } = options;
|
|
121
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
122
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
123
|
+
}
|
|
124
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
125
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
126
|
+
}
|
|
127
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
128
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
129
|
+
return {
|
|
130
|
+
async maybeCompact(store, conversationId) {
|
|
131
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
132
|
+
if (turns.length < threshold) return;
|
|
133
|
+
const older = turns.slice(0, turns.length - keep);
|
|
134
|
+
let summary;
|
|
135
|
+
let timer;
|
|
136
|
+
try {
|
|
137
|
+
summary = await Promise.race([
|
|
138
|
+
summarize({ conversationId, turns: older }),
|
|
139
|
+
new Promise((_, reject) => {
|
|
140
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
141
|
+
})
|
|
142
|
+
]);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger?.error(
|
|
145
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
146
|
+
error
|
|
147
|
+
);
|
|
148
|
+
return;
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
}
|
|
152
|
+
const trimmed = summary.trim();
|
|
153
|
+
if (!trimmed) return;
|
|
154
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
155
|
+
const recent = fresh.slice(older.length);
|
|
156
|
+
await store.clear(conversationId);
|
|
157
|
+
await store.append(conversationId, {
|
|
158
|
+
role: "assistant",
|
|
159
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
160
|
+
});
|
|
161
|
+
for (const turn of recent) {
|
|
162
|
+
await store.append(conversationId, turn);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
27
168
|
// src/history/tursoStore.ts
|
|
28
169
|
var schemaReady = /* @__PURE__ */ new WeakMap();
|
|
29
170
|
function ensureHistorySchema(client, tableName) {
|
|
@@ -117,5 +258,7 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
|
|
|
117
258
|
}
|
|
118
259
|
// Annotate the CommonJS export names for ESM import in node:
|
|
119
260
|
0 && (module.exports = {
|
|
261
|
+
SUMMARY_PREFIX,
|
|
262
|
+
createHistoryCompactor,
|
|
120
263
|
createTursoHistoryStore
|
|
121
264
|
});
|
package/dist/history/index.mjs
CHANGED
|
@@ -1,3 +1,142 @@
|
|
|
1
|
+
// src/core/guardrails.ts
|
|
2
|
+
function detectLeakage(text) {
|
|
3
|
+
const flags = [
|
|
4
|
+
/system prompt/i,
|
|
5
|
+
/hidden instruction/i,
|
|
6
|
+
/here (are|is) (my|the) rules/i,
|
|
7
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
8
|
+
/tool instructions/i
|
|
9
|
+
];
|
|
10
|
+
return flags.some((rx) => rx.test(text));
|
|
11
|
+
}
|
|
12
|
+
function scrubOutput(text) {
|
|
13
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
14
|
+
}
|
|
15
|
+
function guardOutput(text) {
|
|
16
|
+
if (detectLeakage(text)) {
|
|
17
|
+
return "Sorry, I can't share internal instructions. How else can I help?";
|
|
18
|
+
}
|
|
19
|
+
return scrubOutput(text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/core/llm.ts
|
|
23
|
+
var DEFAULT_MODEL = "gpt-4o";
|
|
24
|
+
async function completeOnce({
|
|
25
|
+
client,
|
|
26
|
+
system,
|
|
27
|
+
messages,
|
|
28
|
+
temperature = 0.2,
|
|
29
|
+
model = DEFAULT_MODEL
|
|
30
|
+
}) {
|
|
31
|
+
const res = await client.chat.completions.create({
|
|
32
|
+
model,
|
|
33
|
+
temperature,
|
|
34
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
35
|
+
});
|
|
36
|
+
const text = res.choices[0]?.message?.content ?? "";
|
|
37
|
+
return {
|
|
38
|
+
content: guardOutput(text),
|
|
39
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/core/answer.ts
|
|
44
|
+
var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
|
|
45
|
+
async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
|
|
46
|
+
const result = await answerFn({ system, messages, mode, sender, conversationId });
|
|
47
|
+
const raw = typeof result === "string" ? result : result?.content;
|
|
48
|
+
const usage = typeof result === "string" ? void 0 : result?.usage;
|
|
49
|
+
return {
|
|
50
|
+
content: guardOutput(typeof raw === "string" ? raw : ""),
|
|
51
|
+
usage: usage ?? noUsage()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function answerOnce({
|
|
55
|
+
answerFn,
|
|
56
|
+
client,
|
|
57
|
+
system,
|
|
58
|
+
messages,
|
|
59
|
+
mode,
|
|
60
|
+
sender,
|
|
61
|
+
conversationId,
|
|
62
|
+
temperature,
|
|
63
|
+
model
|
|
64
|
+
}) {
|
|
65
|
+
if (answerFn) {
|
|
66
|
+
return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
|
|
67
|
+
}
|
|
68
|
+
return completeOnce({ client, system, messages, temperature, model });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/history/compaction.ts
|
|
72
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
73
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
74
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
75
|
+
function defaultSummarize(client, model) {
|
|
76
|
+
return async ({ turns }) => {
|
|
77
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
78
|
+
const { content } = await answerOnce({
|
|
79
|
+
client,
|
|
80
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
81
|
+
messages: [{ role: "user", content: transcript }],
|
|
82
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
83
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
84
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
85
|
+
mode: "public",
|
|
86
|
+
model
|
|
87
|
+
});
|
|
88
|
+
return content;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function createHistoryCompactor(deps, options) {
|
|
92
|
+
const { threshold, keep, logger } = options;
|
|
93
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
94
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
95
|
+
}
|
|
96
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
97
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
98
|
+
}
|
|
99
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
100
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
101
|
+
return {
|
|
102
|
+
async maybeCompact(store, conversationId) {
|
|
103
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
104
|
+
if (turns.length < threshold) return;
|
|
105
|
+
const older = turns.slice(0, turns.length - keep);
|
|
106
|
+
let summary;
|
|
107
|
+
let timer;
|
|
108
|
+
try {
|
|
109
|
+
summary = await Promise.race([
|
|
110
|
+
summarize({ conversationId, turns: older }),
|
|
111
|
+
new Promise((_, reject) => {
|
|
112
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
113
|
+
})
|
|
114
|
+
]);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger?.error(
|
|
117
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
118
|
+
error
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
} finally {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
}
|
|
124
|
+
const trimmed = summary.trim();
|
|
125
|
+
if (!trimmed) return;
|
|
126
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
127
|
+
const recent = fresh.slice(older.length);
|
|
128
|
+
await store.clear(conversationId);
|
|
129
|
+
await store.append(conversationId, {
|
|
130
|
+
role: "assistant",
|
|
131
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
132
|
+
});
|
|
133
|
+
for (const turn of recent) {
|
|
134
|
+
await store.append(conversationId, turn);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
1
140
|
// src/history/tursoStore.ts
|
|
2
141
|
var schemaReady = /* @__PURE__ */ new WeakMap();
|
|
3
142
|
function ensureHistorySchema(client, tableName) {
|
|
@@ -90,5 +229,7 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
|
|
|
90
229
|
};
|
|
91
230
|
}
|
|
92
231
|
export {
|
|
232
|
+
SUMMARY_PREFIX,
|
|
233
|
+
createHistoryCompactor,
|
|
93
234
|
createTursoHistoryStore
|
|
94
235
|
};
|
package/package.json
CHANGED