koishi-plugin-chatluna 1.3.4 → 1.3.6

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.
@@ -46,7 +46,7 @@ export declare class ChainMiddleware {
46
46
  constructor(name: string, execute: ChainMiddlewareFunction, graph: ChatChainDependencyGraph);
47
47
  before<T extends keyof ChainMiddlewareName>(name: T): this;
48
48
  after<T extends keyof ChainMiddlewareName>(name: T): this;
49
- run(session: Session, options: ChainMiddlewareContext): Promise<string | ChainMiddlewareRunStatus | h[] | h[][]>;
49
+ run(session: Session, options: ChainMiddlewareContext): Promise<string | h[] | ChainMiddlewareRunStatus | h[][]>;
50
50
  }
51
51
  export interface ChainMiddlewareContext {
52
52
  config: Config;
package/lib/index.cjs CHANGED
@@ -1046,9 +1046,13 @@ var ChatLunaChatChain = class _ChatLunaChatChain extends import_base.ChatLunaLLM
1046
1046
  requests["variables"] = Object.assign(variables ?? {}, {
1047
1047
  prompt: (0, import_string.getMessageContent)(message.content)
1048
1048
  });
1049
+ requests["variables"]["built"] = {
1050
+ conversationId
1051
+ };
1049
1052
  requests["variables_hide"] = requests["variables"];
1050
1053
  requests["configurable"] = {
1051
- session
1054
+ session,
1055
+ conversationId
1052
1056
  };
1053
1057
  requests["id"] = conversationId;
1054
1058
  const response = await (0, import_base.callChatLunaChain)(
@@ -1185,12 +1189,16 @@ var ChatLunaPluginChain = class _ChatLunaPluginChain extends import_base2.ChatLu
1185
1189
  requests["variables"] = Object.assign(variables ?? {}, {
1186
1190
  prompt: (0, import_string2.getMessageContent)(message.content)
1187
1191
  });
1192
+ requests["variables"]["built"] = {
1193
+ conversationId
1194
+ };
1188
1195
  requests["after_user_message"] = new import_messages2.HumanMessage(
1189
1196
  AGENT_AFTER_USER_PROMPT
1190
1197
  );
1191
1198
  requests["variables_hide"] = requests["variables"];
1192
1199
  requests["configurable"] = {
1193
- session
1200
+ session,
1201
+ conversationId
1194
1202
  };
1195
1203
  this._toolsRef.update(session, messages.concat(message));
1196
1204
  const preset = this.preset.value;
@@ -5816,6 +5824,7 @@ var import_crypto7 = require("crypto");
5816
5824
  var logger11;
5817
5825
  function apply56(ctx, config, chain) {
5818
5826
  logger11 = (0, import_logger7.createLogger)(ctx);
5827
+ const selectRoomForSession = /* @__PURE__ */ __name(async (session, joinedRooms) => pickContextualRoom(ctx, session, config, joinedRooms), "selectRoomForSession");
5819
5828
  chain.middleware("resolve_room", async (session, context) => {
5820
5829
  let joinRoom = await queryJoinedConversationRoom(
5821
5830
  ctx,
@@ -5858,32 +5867,16 @@ function apply56(ctx, config, chain) {
5858
5867
  session
5859
5868
  );
5860
5869
  if (joinedRooms.length > 0) {
5861
- joinRoom = // 优先加入自己创建的房间
5862
- joinedRooms.find(
5863
- (room) => room.visibility === "private" && room.roomMasterId === session.userId
5864
- ) ?? // 优先加入自己创建的房间
5865
- joinedRooms.find(
5866
- (room) => room.visibility === "template_clone" && room.roomMasterId === session.userId
5870
+ joinRoom = await selectRoomForSession(session, joinedRooms);
5871
+ }
5872
+ if (joinRoom != null) {
5873
+ await switchConversationRoom(ctx, session, joinRoom.roomId);
5874
+ logger11.success(
5875
+ session.text("chatluna.room.auto_switch", [
5876
+ session.userId,
5877
+ joinRoom.roomName
5878
+ ])
5867
5879
  );
5868
- if (config.autoCreateRoomFromUser !== true && joinRoom == null) {
5869
- joinRoom = // 优先加入模版克隆房间
5870
- joinedRooms.find(
5871
- (room) => room.visibility === "template_clone"
5872
- ) ?? joinedRooms[Math.floor(Math.random() * joinedRooms.length)];
5873
- }
5874
- if (joinRoom != null) {
5875
- await switchConversationRoom(
5876
- ctx,
5877
- session,
5878
- joinRoom.roomId
5879
- );
5880
- logger11.success(
5881
- session.text("chatluna.room.auto_switch", [
5882
- session.userId,
5883
- joinRoom.roomName
5884
- ])
5885
- );
5886
- }
5887
5880
  }
5888
5881
  }
5889
5882
  if (joinRoom == null && config.autoCreateRoomFromUser !== true && !session.isDirect && (context.command?.length ?? 0) < 1) {
@@ -5979,6 +5972,34 @@ function apply56(ctx, config, chain) {
5979
5972
  }).after("lifecycle-prepare");
5980
5973
  }
5981
5974
  __name(apply56, "apply");
5975
+ async function pickContextualRoom(ctx, session, config, joinedRooms) {
5976
+ if (joinedRooms.length === 0) {
5977
+ return void 0;
5978
+ }
5979
+ if (!session.isDirect && config.autoCreateRoomFromUser !== true) {
5980
+ const groupRooms = await ctx.database.get("chathub_room_group_member", {
5981
+ groupId: session.guildId,
5982
+ roomId: { $in: joinedRooms.map((room) => room.roomId) }
5983
+ });
5984
+ if (groupRooms.length > 0) {
5985
+ const scopedRoomIds = new Set(groupRooms.map((room) => room.roomId));
5986
+ const findScopedRoom = /* @__PURE__ */ __name((matcher) => joinedRooms.find((room) => {
5987
+ if (!scopedRoomIds.has(room.roomId)) {
5988
+ return false;
5989
+ }
5990
+ return matcher ? matcher(room) : true;
5991
+ }), "findScopedRoom");
5992
+ return findScopedRoom(
5993
+ (room) => room.visibility === "template_clone"
5994
+ ) ?? findScopedRoom();
5995
+ }
5996
+ return void 0;
5997
+ }
5998
+ return joinedRooms.find(
5999
+ (room) => room.visibility === "private" && room.roomMasterId === session.userId
6000
+ ) ?? joinedRooms.find((room) => room.visibility === "private") ?? joinedRooms.find((room) => room.visibility !== "template_clone") ?? joinedRooms[0];
6001
+ }
6002
+ __name(pickContextualRoom, "pickContextualRoom");
5982
6003
 
5983
6004
  // src/middlewares/room/room_info.ts
5984
6005
  function apply57(ctx, config, chain) {
package/lib/index.mjs CHANGED
@@ -1021,9 +1021,13 @@ var ChatLunaChatChain = class _ChatLunaChatChain extends ChatLunaLLMChainWrapper
1021
1021
  requests["variables"] = Object.assign(variables ?? {}, {
1022
1022
  prompt: getMessageContent(message.content)
1023
1023
  });
1024
+ requests["variables"]["built"] = {
1025
+ conversationId
1026
+ };
1024
1027
  requests["variables_hide"] = requests["variables"];
1025
1028
  requests["configurable"] = {
1026
- session
1029
+ session,
1030
+ conversationId
1027
1031
  };
1028
1032
  requests["id"] = conversationId;
1029
1033
  const response = await callChatLunaChain(
@@ -1168,12 +1172,16 @@ var ChatLunaPluginChain = class _ChatLunaPluginChain extends ChatLunaLLMChainWra
1168
1172
  requests["variables"] = Object.assign(variables ?? {}, {
1169
1173
  prompt: getMessageContent2(message.content)
1170
1174
  });
1175
+ requests["variables"]["built"] = {
1176
+ conversationId
1177
+ };
1171
1178
  requests["after_user_message"] = new HumanMessage(
1172
1179
  AGENT_AFTER_USER_PROMPT
1173
1180
  );
1174
1181
  requests["variables_hide"] = requests["variables"];
1175
1182
  requests["configurable"] = {
1176
- session
1183
+ session,
1184
+ conversationId
1177
1185
  };
1178
1186
  this._toolsRef.update(session, messages.concat(message));
1179
1187
  const preset = this.preset.value;
@@ -5826,6 +5834,7 @@ import { randomUUID as randomUUID4 } from "crypto";
5826
5834
  var logger11;
5827
5835
  function apply56(ctx, config, chain) {
5828
5836
  logger11 = createLogger7(ctx);
5837
+ const selectRoomForSession = /* @__PURE__ */ __name(async (session, joinedRooms) => pickContextualRoom(ctx, session, config, joinedRooms), "selectRoomForSession");
5829
5838
  chain.middleware("resolve_room", async (session, context) => {
5830
5839
  let joinRoom = await queryJoinedConversationRoom(
5831
5840
  ctx,
@@ -5868,32 +5877,16 @@ function apply56(ctx, config, chain) {
5868
5877
  session
5869
5878
  );
5870
5879
  if (joinedRooms.length > 0) {
5871
- joinRoom = // 优先加入自己创建的房间
5872
- joinedRooms.find(
5873
- (room) => room.visibility === "private" && room.roomMasterId === session.userId
5874
- ) ?? // 优先加入自己创建的房间
5875
- joinedRooms.find(
5876
- (room) => room.visibility === "template_clone" && room.roomMasterId === session.userId
5880
+ joinRoom = await selectRoomForSession(session, joinedRooms);
5881
+ }
5882
+ if (joinRoom != null) {
5883
+ await switchConversationRoom(ctx, session, joinRoom.roomId);
5884
+ logger11.success(
5885
+ session.text("chatluna.room.auto_switch", [
5886
+ session.userId,
5887
+ joinRoom.roomName
5888
+ ])
5877
5889
  );
5878
- if (config.autoCreateRoomFromUser !== true && joinRoom == null) {
5879
- joinRoom = // 优先加入模版克隆房间
5880
- joinedRooms.find(
5881
- (room) => room.visibility === "template_clone"
5882
- ) ?? joinedRooms[Math.floor(Math.random() * joinedRooms.length)];
5883
- }
5884
- if (joinRoom != null) {
5885
- await switchConversationRoom(
5886
- ctx,
5887
- session,
5888
- joinRoom.roomId
5889
- );
5890
- logger11.success(
5891
- session.text("chatluna.room.auto_switch", [
5892
- session.userId,
5893
- joinRoom.roomName
5894
- ])
5895
- );
5896
- }
5897
5890
  }
5898
5891
  }
5899
5892
  if (joinRoom == null && config.autoCreateRoomFromUser !== true && !session.isDirect && (context.command?.length ?? 0) < 1) {
@@ -5989,6 +5982,34 @@ function apply56(ctx, config, chain) {
5989
5982
  }).after("lifecycle-prepare");
5990
5983
  }
5991
5984
  __name(apply56, "apply");
5985
+ async function pickContextualRoom(ctx, session, config, joinedRooms) {
5986
+ if (joinedRooms.length === 0) {
5987
+ return void 0;
5988
+ }
5989
+ if (!session.isDirect && config.autoCreateRoomFromUser !== true) {
5990
+ const groupRooms = await ctx.database.get("chathub_room_group_member", {
5991
+ groupId: session.guildId,
5992
+ roomId: { $in: joinedRooms.map((room) => room.roomId) }
5993
+ });
5994
+ if (groupRooms.length > 0) {
5995
+ const scopedRoomIds = new Set(groupRooms.map((room) => room.roomId));
5996
+ const findScopedRoom = /* @__PURE__ */ __name((matcher) => joinedRooms.find((room) => {
5997
+ if (!scopedRoomIds.has(room.roomId)) {
5998
+ return false;
5999
+ }
6000
+ return matcher ? matcher(room) : true;
6001
+ }), "findScopedRoom");
6002
+ return findScopedRoom(
6003
+ (room) => room.visibility === "template_clone"
6004
+ ) ?? findScopedRoom();
6005
+ }
6006
+ return void 0;
6007
+ }
6008
+ return joinedRooms.find(
6009
+ (room) => room.visibility === "private" && room.roomMasterId === session.userId
6010
+ ) ?? joinedRooms.find((room) => room.visibility === "private") ?? joinedRooms.find((room) => room.visibility !== "template_clone") ?? joinedRooms[0];
6011
+ }
6012
+ __name(pickContextualRoom, "pickContextualRoom");
5992
6013
 
5993
6014
  // src/middlewares/room/room_info.ts
5994
6015
  function apply57(ctx, config, chain) {
@@ -22,4 +22,4 @@ export type CreateOpenAIAgentParams = {
22
22
  };
23
23
  export declare function createOpenAIAgent({ llm, tools, prompt }: CreateOpenAIAgentParams): RunnableSequence<{
24
24
  steps: AgentStep[];
25
- }, AgentAction | AgentAction[] | AgentFinish>;
25
+ }, AgentAction | AgentFinish | AgentAction[]>;
@@ -188,12 +188,40 @@ var InfiniteContextManager = class {
188
188
  import_koishi_plugin_chatluna.logger.info(
189
189
  `[InfiniteContext] Start compression with total tokens: ${totalTokens}, threshold: ${threshold}`
190
190
  );
191
+ const filteredMessages = messages.filter(
192
+ (message) => !this._isToolRelatedMessage(message)
193
+ );
194
+ if (filteredMessages.length === 0) {
195
+ return;
196
+ }
197
+ const filteredStats = await this._calculateMessageTokenStats(
198
+ model,
199
+ filteredMessages
200
+ );
201
+ const filteredTotalTokens = filteredStats.reduce((sum, current) => sum + current.tokens, 0) + presetTokens;
202
+ if (filteredTotalTokens <= threshold) {
203
+ await this._rewriteChatHistory(filteredMessages);
204
+ import_koishi_plugin_chatluna.logger.info(
205
+ "[InfiniteContext] Filtered tool-related messages reduced tokens from %d to %d",
206
+ totalTokens,
207
+ filteredTotalTokens
208
+ );
209
+ return;
210
+ }
191
211
  const compressionResult = await this._compressMessages(
192
212
  wrapper,
193
- stats,
213
+ filteredStats,
194
214
  maxTokenLimit
195
215
  );
196
216
  if (!compressionResult) {
217
+ if (filteredMessages.length !== messages.length) {
218
+ await this._rewriteChatHistory(filteredMessages);
219
+ import_koishi_plugin_chatluna.logger.info(
220
+ "[InfiniteContext] Filtered tool-related messages (compression skipped) reduced tokens from %d to %d",
221
+ totalTokens,
222
+ filteredTotalTokens
223
+ );
224
+ }
197
225
  return;
198
226
  }
199
227
  const { messages: rewrittenMessages, tokenCount } = compressionResult;
@@ -246,12 +274,9 @@ var InfiniteContextManager = class {
246
274
  0,
247
275
  contentStats.length - preserveCount
248
276
  );
249
- compressible = contentStats.filter((stat, index) => {
250
- if (this._isCompressedMessage(stat.message)) {
251
- return false;
252
- }
253
- return index < thresholdIndex;
254
- });
277
+ compressible = contentStats.filter(
278
+ (stat, index) => index < thresholdIndex
279
+ );
255
280
  if (compressible.length > 0) {
256
281
  break;
257
282
  }
@@ -272,10 +297,16 @@ var InfiniteContextManager = class {
272
297
  return null;
273
298
  }
274
299
  const compressor = this._ensureInfiniteContextChain(wrapper);
275
- const compressedSegments = [];
300
+ const chunkSummaries = [];
301
+ const previousSummaries = compressible.filter((stat) => this._isCompressedMessage(stat.message)).map(
302
+ (stat) => (0, import_string2.getMessageContent)(stat.message.content)?.trim() ?? ""
303
+ ).filter((text) => text.length > 0);
276
304
  for (let index = 0; index < chunkStats.length; index++) {
277
305
  const chunk = chunkStats[index];
278
- const chunkMessages = chunk.map((item) => item.message);
306
+ const chunkMessages = chunk.map((item) => item.message).filter((message) => !this._isCompressedMessage(message));
307
+ if (chunkMessages.length === 0) {
308
+ continue;
309
+ }
279
310
  const chunkText = this._formatChunkForCompression(chunkMessages);
280
311
  const compressedText = await compressor.compressChunk({
281
312
  chunk: chunkText,
@@ -284,40 +315,28 @@ var InfiniteContextManager = class {
284
315
  if (!compressedText) {
285
316
  continue;
286
317
  }
287
- compressedSegments.push({
318
+ chunkSummaries.push({
288
319
  content: compressedText,
289
320
  chunkIndex: index,
290
321
  chunkSize: chunkMessages.length
291
322
  });
292
323
  }
293
- if (compressedSegments.length === 0) {
324
+ if (chunkSummaries.length === 0 && previousSummaries.length === 0) {
325
+ return null;
326
+ }
327
+ const finalSummary = await this._buildFinalSummary(
328
+ compressor,
329
+ previousSummaries,
330
+ chunkSummaries
331
+ );
332
+ if (!finalSummary) {
294
333
  return null;
295
334
  }
296
- const aggregatedSummary = compressedSegments.map(
297
- (segment, idx) => `### Segment ${idx + 1}
298
- ${segment.content.trim()}`
299
- ).join("\n\n");
300
- const summaryMeta = {
301
- source: "infinite-context",
302
- segments: compressedSegments.length,
303
- segmentDetail: compressedSegments.map((segment, idx) => ({
304
- segment: idx + 1,
305
- chunkIndex: segment.chunkIndex,
306
- chunkSize: segment.chunkSize
307
- }))
308
- };
309
335
  const compressedMessages = [
310
336
  new import_messages.HumanMessage({
311
- content: `<infinite_context segments="${compressedSegments.length}">
312
- ${aggregatedSummary}
313
- </infinite_context>`,
337
+ content: finalSummary.content,
314
338
  name: "infinite_context",
315
- additional_kwargs: summaryMeta
316
- }),
317
- new import_messages.AIMessage({
318
- content: "Summary acknowledged. I will prioritise available tools when future turns involve these archived topics, and if no tools are available I will note the memory gap and ask the user to confirm the details.",
319
- name: "infinite_context_ack",
320
- additional_kwargs: summaryMeta
339
+ additional_kwargs: finalSummary.meta
321
340
  })
322
341
  ];
323
342
  const mergedMessages = [
@@ -345,6 +364,32 @@ ${aggregatedSummary}
345
364
  const content = (0, import_string2.getMessageContent)(message?.content ?? "");
346
365
  return typeof content === "string" && /<\/?infinite_context/iu.test(content);
347
366
  }
367
+ _isToolRelatedMessage(message) {
368
+ if (message.getType() === "tool") {
369
+ return true;
370
+ }
371
+ const anyMessage = message;
372
+ if (Array.isArray(anyMessage.tool_calls) && anyMessage.tool_calls.length > 0) {
373
+ return true;
374
+ }
375
+ const additionalToolCalls = anyMessage.additional_kwargs?.["tool_calls"];
376
+ return Array.isArray(additionalToolCalls) && additionalToolCalls.length > 0;
377
+ }
378
+ async _rewriteChatHistory(messages) {
379
+ const additionalArgs = {
380
+ ...await this.options.chatHistory.getAdditionalArgs()
381
+ };
382
+ await this.options.chatHistory.clear();
383
+ for (const message of messages) {
384
+ await this.options.chatHistory.addMessage(message);
385
+ }
386
+ if (Object.keys(additionalArgs).length > 0) {
387
+ await this.options.chatHistory.overrideAdditionalArgs(
388
+ additionalArgs
389
+ );
390
+ }
391
+ await this.options.chatHistory.loadConversation();
392
+ }
348
393
  _splitChunksForCompression(stats, maxTokenLimit) {
349
394
  const chunkTokenTarget = Math.max(Math.floor(maxTokenLimit * 0.15), 300);
350
395
  const chunks = [];
@@ -384,33 +429,8 @@ ${aggregatedSummary}
384
429
  const role = message.getType().toUpperCase();
385
430
  const nameSuffix = message.name ? ` (${message.name})` : "";
386
431
  const content = (0, import_string2.getMessageContent)(message.content).trim();
387
- const extras = [];
388
- const aiMessage = message;
389
- if (Array.isArray(aiMessage.tool_calls) && aiMessage.tool_calls.length > 0) {
390
- const toolCalls = aiMessage.tool_calls.map((call) => {
391
- let args = "";
392
- try {
393
- args = JSON.stringify(call.args);
394
- } catch (error) {
395
- args = "[unserializable]";
396
- }
397
- return `${call.name} => ${args}`;
398
- }).join("; ");
399
- extras.push(`tool calls: ${toolCalls}`);
400
- }
401
- if (message.getType() === "tool") {
402
- const toolMessage = message;
403
- if (toolMessage.tool_call_id) {
404
- extras.push(`tool call id: ${toolMessage.tool_call_id}`);
405
- }
406
- if (toolMessage.name) {
407
- extras.push(`tool name: ${toolMessage.name}`);
408
- }
409
- }
410
- const extraText = extras.length > 0 ? `
411
- Meta: ${extras.join(", ")}` : "";
412
432
  return `[${role}${nameSuffix}]
413
- ${content || "(empty)"}${extraText}`;
433
+ ${content || "(empty)"}`;
414
434
  }).join("\n---\n");
415
435
  }
416
436
  _ensureInfiniteContextChain(wrapper) {
@@ -421,6 +441,64 @@ ${content || "(empty)"}${extraText}`;
421
441
  }
422
442
  return this._chain;
423
443
  }
444
+ async _buildFinalSummary(compressor, previousSummaries, chunkSummaries) {
445
+ const cleanedPrevious = previousSummaries.filter(
446
+ (text) => text.trim().length > 0
447
+ );
448
+ const cleanedChunks = chunkSummaries.filter(
449
+ (summary) => summary.content.trim().length > 0
450
+ );
451
+ if (cleanedPrevious.length === 0 && cleanedChunks.length === 0) {
452
+ return null;
453
+ }
454
+ if (cleanedPrevious.length === 0 && cleanedChunks.length === 1) {
455
+ return {
456
+ content: cleanedChunks[0].content.trim(),
457
+ meta: {
458
+ source: "infinite-context",
459
+ mergedSegments: 1,
460
+ previousSummaries: 0,
461
+ chunkDetail: [
462
+ {
463
+ chunkIndex: cleanedChunks[0].chunkIndex,
464
+ chunkSize: cleanedChunks[0].chunkSize
465
+ }
466
+ ]
467
+ }
468
+ };
469
+ }
470
+ const virtualTranscript = [];
471
+ if (cleanedPrevious.length > 0) {
472
+ virtualTranscript.push(
473
+ `Existing summary snapshot:
474
+ ${cleanedPrevious.join("\n\n")}`
475
+ );
476
+ }
477
+ cleanedChunks.forEach((chunk, index) => {
478
+ virtualTranscript.push(
479
+ `Recent segment ${index + 1} (${chunk.chunkSize} turns):
480
+ ${chunk.content}`
481
+ );
482
+ });
483
+ const mergedInput = virtualTranscript.join("\n\n---\n\n");
484
+ const refined = await compressor.compressChunk({
485
+ chunk: mergedInput,
486
+ conversationId: this.options.conversationId
487
+ });
488
+ const content = refined?.trim() || mergedInput;
489
+ return {
490
+ content,
491
+ meta: {
492
+ source: "infinite-context",
493
+ mergedSegments: cleanedChunks.length,
494
+ previousSummaries: cleanedPrevious.length,
495
+ chunkDetail: cleanedChunks.map((chunk) => ({
496
+ chunkIndex: chunk.chunkIndex,
497
+ chunkSize: chunk.chunkSize
498
+ }))
499
+ }
500
+ };
501
+ }
424
502
  };
425
503
 
426
504
  // src/llm-core/chat/app.ts
@@ -445,7 +523,7 @@ var ChatInterface = class {
445
523
  _historyMemory;
446
524
  _infiniteContextManager;
447
525
  _chatCount = 0;
448
- async handleChatError(arg, wrapper, error) {
526
+ async handleChatError(arg, wrapper, error, throwError = true) {
449
527
  await this.ctx.parallel(
450
528
  "chatluna/after-chat-error",
451
529
  error,
@@ -455,6 +533,9 @@ var ChatInterface = class {
455
533
  this,
456
534
  wrapper
457
535
  );
536
+ if (!throwError) {
537
+ return;
538
+ }
458
539
  if (error instanceof import_error2.ChatLunaError && error.errorCode === import_error2.ChatLunaErrorCode.API_UNSAFE_CONTENT) {
459
540
  throw error;
460
541
  }
@@ -561,15 +642,19 @@ var ChatInterface = class {
561
642
  }
562
643
  await this.chatHistory.addMessage(saveMessage);
563
644
  }
564
- this.ctx.parallel(
565
- "chatluna/after-chat",
566
- arg.conversationId,
567
- arg.message,
568
- displayResponse,
569
- { ...arg.variables, chatCount: this._chatCount },
570
- this,
571
- arg.session
572
- );
645
+ try {
646
+ await this.ctx.parallel(
647
+ "chatluna/after-chat",
648
+ arg.conversationId,
649
+ arg.message,
650
+ displayResponse,
651
+ { ...arg.variables, chatCount: this._chatCount },
652
+ this,
653
+ arg.session
654
+ );
655
+ } catch (error) {
656
+ await this.handleChatError(arg, wrapper, error, false);
657
+ }
573
658
  return { message: displayResponse };
574
659
  }
575
660
  async handlePostProcessing(arg, message) {
@@ -20,15 +20,12 @@ import { ChatLunaChatModel } from "koishi-plugin-chatluna/llm-core/platform/mode
20
20
  import {
21
21
  ModelCapabilities
22
22
  } from "koishi-plugin-chatluna/llm-core/platform/types";
23
- import { AIMessage as AIMessage2, ToolMessage as ToolMessage2 } from "@langchain/core/messages";
23
+ import { AIMessage, ToolMessage } from "@langchain/core/messages";
24
24
  import { getMessageContent as getMessageContent3 } from "koishi-plugin-chatluna/utils/string";
25
25
  import { computed } from "@vue/reactivity";
26
26
 
27
27
  // src/llm-core/chat/infinite_context.ts
28
- import {
29
- AIMessage,
30
- HumanMessage
31
- } from "@langchain/core/messages";
28
+ import { HumanMessage } from "@langchain/core/messages";
32
29
  import { getMessageContent as getMessageContent2 } from "koishi-plugin-chatluna/utils/string";
33
30
  import { logger } from "koishi-plugin-chatluna";
34
31
 
@@ -185,12 +182,40 @@ var InfiniteContextManager = class {
185
182
  logger.info(
186
183
  `[InfiniteContext] Start compression with total tokens: ${totalTokens}, threshold: ${threshold}`
187
184
  );
185
+ const filteredMessages = messages.filter(
186
+ (message) => !this._isToolRelatedMessage(message)
187
+ );
188
+ if (filteredMessages.length === 0) {
189
+ return;
190
+ }
191
+ const filteredStats = await this._calculateMessageTokenStats(
192
+ model,
193
+ filteredMessages
194
+ );
195
+ const filteredTotalTokens = filteredStats.reduce((sum, current) => sum + current.tokens, 0) + presetTokens;
196
+ if (filteredTotalTokens <= threshold) {
197
+ await this._rewriteChatHistory(filteredMessages);
198
+ logger.info(
199
+ "[InfiniteContext] Filtered tool-related messages reduced tokens from %d to %d",
200
+ totalTokens,
201
+ filteredTotalTokens
202
+ );
203
+ return;
204
+ }
188
205
  const compressionResult = await this._compressMessages(
189
206
  wrapper,
190
- stats,
207
+ filteredStats,
191
208
  maxTokenLimit
192
209
  );
193
210
  if (!compressionResult) {
211
+ if (filteredMessages.length !== messages.length) {
212
+ await this._rewriteChatHistory(filteredMessages);
213
+ logger.info(
214
+ "[InfiniteContext] Filtered tool-related messages (compression skipped) reduced tokens from %d to %d",
215
+ totalTokens,
216
+ filteredTotalTokens
217
+ );
218
+ }
194
219
  return;
195
220
  }
196
221
  const { messages: rewrittenMessages, tokenCount } = compressionResult;
@@ -243,12 +268,9 @@ var InfiniteContextManager = class {
243
268
  0,
244
269
  contentStats.length - preserveCount
245
270
  );
246
- compressible = contentStats.filter((stat, index) => {
247
- if (this._isCompressedMessage(stat.message)) {
248
- return false;
249
- }
250
- return index < thresholdIndex;
251
- });
271
+ compressible = contentStats.filter(
272
+ (stat, index) => index < thresholdIndex
273
+ );
252
274
  if (compressible.length > 0) {
253
275
  break;
254
276
  }
@@ -269,10 +291,16 @@ var InfiniteContextManager = class {
269
291
  return null;
270
292
  }
271
293
  const compressor = this._ensureInfiniteContextChain(wrapper);
272
- const compressedSegments = [];
294
+ const chunkSummaries = [];
295
+ const previousSummaries = compressible.filter((stat) => this._isCompressedMessage(stat.message)).map(
296
+ (stat) => getMessageContent2(stat.message.content)?.trim() ?? ""
297
+ ).filter((text) => text.length > 0);
273
298
  for (let index = 0; index < chunkStats.length; index++) {
274
299
  const chunk = chunkStats[index];
275
- const chunkMessages = chunk.map((item) => item.message);
300
+ const chunkMessages = chunk.map((item) => item.message).filter((message) => !this._isCompressedMessage(message));
301
+ if (chunkMessages.length === 0) {
302
+ continue;
303
+ }
276
304
  const chunkText = this._formatChunkForCompression(chunkMessages);
277
305
  const compressedText = await compressor.compressChunk({
278
306
  chunk: chunkText,
@@ -281,40 +309,28 @@ var InfiniteContextManager = class {
281
309
  if (!compressedText) {
282
310
  continue;
283
311
  }
284
- compressedSegments.push({
312
+ chunkSummaries.push({
285
313
  content: compressedText,
286
314
  chunkIndex: index,
287
315
  chunkSize: chunkMessages.length
288
316
  });
289
317
  }
290
- if (compressedSegments.length === 0) {
318
+ if (chunkSummaries.length === 0 && previousSummaries.length === 0) {
319
+ return null;
320
+ }
321
+ const finalSummary = await this._buildFinalSummary(
322
+ compressor,
323
+ previousSummaries,
324
+ chunkSummaries
325
+ );
326
+ if (!finalSummary) {
291
327
  return null;
292
328
  }
293
- const aggregatedSummary = compressedSegments.map(
294
- (segment, idx) => `### Segment ${idx + 1}
295
- ${segment.content.trim()}`
296
- ).join("\n\n");
297
- const summaryMeta = {
298
- source: "infinite-context",
299
- segments: compressedSegments.length,
300
- segmentDetail: compressedSegments.map((segment, idx) => ({
301
- segment: idx + 1,
302
- chunkIndex: segment.chunkIndex,
303
- chunkSize: segment.chunkSize
304
- }))
305
- };
306
329
  const compressedMessages = [
307
330
  new HumanMessage({
308
- content: `<infinite_context segments="${compressedSegments.length}">
309
- ${aggregatedSummary}
310
- </infinite_context>`,
331
+ content: finalSummary.content,
311
332
  name: "infinite_context",
312
- additional_kwargs: summaryMeta
313
- }),
314
- new AIMessage({
315
- content: "Summary acknowledged. I will prioritise available tools when future turns involve these archived topics, and if no tools are available I will note the memory gap and ask the user to confirm the details.",
316
- name: "infinite_context_ack",
317
- additional_kwargs: summaryMeta
333
+ additional_kwargs: finalSummary.meta
318
334
  })
319
335
  ];
320
336
  const mergedMessages = [
@@ -342,6 +358,32 @@ ${aggregatedSummary}
342
358
  const content = getMessageContent2(message?.content ?? "");
343
359
  return typeof content === "string" && /<\/?infinite_context/iu.test(content);
344
360
  }
361
+ _isToolRelatedMessage(message) {
362
+ if (message.getType() === "tool") {
363
+ return true;
364
+ }
365
+ const anyMessage = message;
366
+ if (Array.isArray(anyMessage.tool_calls) && anyMessage.tool_calls.length > 0) {
367
+ return true;
368
+ }
369
+ const additionalToolCalls = anyMessage.additional_kwargs?.["tool_calls"];
370
+ return Array.isArray(additionalToolCalls) && additionalToolCalls.length > 0;
371
+ }
372
+ async _rewriteChatHistory(messages) {
373
+ const additionalArgs = {
374
+ ...await this.options.chatHistory.getAdditionalArgs()
375
+ };
376
+ await this.options.chatHistory.clear();
377
+ for (const message of messages) {
378
+ await this.options.chatHistory.addMessage(message);
379
+ }
380
+ if (Object.keys(additionalArgs).length > 0) {
381
+ await this.options.chatHistory.overrideAdditionalArgs(
382
+ additionalArgs
383
+ );
384
+ }
385
+ await this.options.chatHistory.loadConversation();
386
+ }
345
387
  _splitChunksForCompression(stats, maxTokenLimit) {
346
388
  const chunkTokenTarget = Math.max(Math.floor(maxTokenLimit * 0.15), 300);
347
389
  const chunks = [];
@@ -381,33 +423,8 @@ ${aggregatedSummary}
381
423
  const role = message.getType().toUpperCase();
382
424
  const nameSuffix = message.name ? ` (${message.name})` : "";
383
425
  const content = getMessageContent2(message.content).trim();
384
- const extras = [];
385
- const aiMessage = message;
386
- if (Array.isArray(aiMessage.tool_calls) && aiMessage.tool_calls.length > 0) {
387
- const toolCalls = aiMessage.tool_calls.map((call) => {
388
- let args = "";
389
- try {
390
- args = JSON.stringify(call.args);
391
- } catch (error) {
392
- args = "[unserializable]";
393
- }
394
- return `${call.name} => ${args}`;
395
- }).join("; ");
396
- extras.push(`tool calls: ${toolCalls}`);
397
- }
398
- if (message.getType() === "tool") {
399
- const toolMessage = message;
400
- if (toolMessage.tool_call_id) {
401
- extras.push(`tool call id: ${toolMessage.tool_call_id}`);
402
- }
403
- if (toolMessage.name) {
404
- extras.push(`tool name: ${toolMessage.name}`);
405
- }
406
- }
407
- const extraText = extras.length > 0 ? `
408
- Meta: ${extras.join(", ")}` : "";
409
426
  return `[${role}${nameSuffix}]
410
- ${content || "(empty)"}${extraText}`;
427
+ ${content || "(empty)"}`;
411
428
  }).join("\n---\n");
412
429
  }
413
430
  _ensureInfiniteContextChain(wrapper) {
@@ -418,6 +435,64 @@ ${content || "(empty)"}${extraText}`;
418
435
  }
419
436
  return this._chain;
420
437
  }
438
+ async _buildFinalSummary(compressor, previousSummaries, chunkSummaries) {
439
+ const cleanedPrevious = previousSummaries.filter(
440
+ (text) => text.trim().length > 0
441
+ );
442
+ const cleanedChunks = chunkSummaries.filter(
443
+ (summary) => summary.content.trim().length > 0
444
+ );
445
+ if (cleanedPrevious.length === 0 && cleanedChunks.length === 0) {
446
+ return null;
447
+ }
448
+ if (cleanedPrevious.length === 0 && cleanedChunks.length === 1) {
449
+ return {
450
+ content: cleanedChunks[0].content.trim(),
451
+ meta: {
452
+ source: "infinite-context",
453
+ mergedSegments: 1,
454
+ previousSummaries: 0,
455
+ chunkDetail: [
456
+ {
457
+ chunkIndex: cleanedChunks[0].chunkIndex,
458
+ chunkSize: cleanedChunks[0].chunkSize
459
+ }
460
+ ]
461
+ }
462
+ };
463
+ }
464
+ const virtualTranscript = [];
465
+ if (cleanedPrevious.length > 0) {
466
+ virtualTranscript.push(
467
+ `Existing summary snapshot:
468
+ ${cleanedPrevious.join("\n\n")}`
469
+ );
470
+ }
471
+ cleanedChunks.forEach((chunk, index) => {
472
+ virtualTranscript.push(
473
+ `Recent segment ${index + 1} (${chunk.chunkSize} turns):
474
+ ${chunk.content}`
475
+ );
476
+ });
477
+ const mergedInput = virtualTranscript.join("\n\n---\n\n");
478
+ const refined = await compressor.compressChunk({
479
+ chunk: mergedInput,
480
+ conversationId: this.options.conversationId
481
+ });
482
+ const content = refined?.trim() || mergedInput;
483
+ return {
484
+ content,
485
+ meta: {
486
+ source: "infinite-context",
487
+ mergedSegments: cleanedChunks.length,
488
+ previousSummaries: cleanedPrevious.length,
489
+ chunkDetail: cleanedChunks.map((chunk) => ({
490
+ chunkIndex: chunk.chunkIndex,
491
+ chunkSize: chunk.chunkSize
492
+ }))
493
+ }
494
+ };
495
+ }
421
496
  };
422
497
 
423
498
  // src/llm-core/chat/app.ts
@@ -442,7 +517,7 @@ var ChatInterface = class {
442
517
  _historyMemory;
443
518
  _infiniteContextManager;
444
519
  _chatCount = 0;
445
- async handleChatError(arg, wrapper, error) {
520
+ async handleChatError(arg, wrapper, error, throwError = true) {
446
521
  await this.ctx.parallel(
447
522
  "chatluna/after-chat-error",
448
523
  error,
@@ -452,6 +527,9 @@ var ChatInterface = class {
452
527
  this,
453
528
  wrapper
454
529
  );
530
+ if (!throwError) {
531
+ return;
532
+ }
455
533
  if (error instanceof ChatLunaError2 && error.errorCode === ChatLunaErrorCode2.API_UNSAFE_CONTENT) {
456
534
  throw error;
457
535
  }
@@ -510,7 +588,7 @@ var ChatInterface = class {
510
588
  maxToken: this.preset?.value?.config?.maxOutputToken
511
589
  });
512
590
  const responseMessage = response.message;
513
- const displayResponse = new AIMessage2({
591
+ const displayResponse = new AIMessage({
514
592
  content: responseMessage.content
515
593
  });
516
594
  displayResponse.additional_kwargs = responseMessage.additional_kwargs;
@@ -536,7 +614,7 @@ var ChatInterface = class {
536
614
  const intermediateSteps = response["parallelIntermediateSteps"];
537
615
  for (const parallelSteps of intermediateSteps) {
538
616
  await this.chatHistory.addMessage(
539
- new AIMessage2({
617
+ new AIMessage({
540
618
  content: "",
541
619
  tool_calls: parallelSteps.map((step) => ({
542
620
  id: step.action.toolCallId,
@@ -547,7 +625,7 @@ var ChatInterface = class {
547
625
  );
548
626
  for (const step of parallelSteps) {
549
627
  await this.chatHistory.addMessage(
550
- new ToolMessage2({
628
+ new ToolMessage({
551
629
  content: step.observation,
552
630
  tool_call_id: step.action.toolCallId,
553
631
  name: step.action.tool
@@ -558,15 +636,19 @@ var ChatInterface = class {
558
636
  }
559
637
  await this.chatHistory.addMessage(saveMessage);
560
638
  }
561
- this.ctx.parallel(
562
- "chatluna/after-chat",
563
- arg.conversationId,
564
- arg.message,
565
- displayResponse,
566
- { ...arg.variables, chatCount: this._chatCount },
567
- this,
568
- arg.session
569
- );
639
+ try {
640
+ await this.ctx.parallel(
641
+ "chatluna/after-chat",
642
+ arg.conversationId,
643
+ arg.message,
644
+ displayResponse,
645
+ { ...arg.variables, chatCount: this._chatCount },
646
+ this,
647
+ arg.session
648
+ );
649
+ } catch (error) {
650
+ await this.handleChatError(arg, wrapper, error, false);
651
+ }
570
652
  return { message: displayResponse };
571
653
  }
572
654
  async handlePostProcessing(arg, message) {
@@ -14,9 +14,12 @@ export declare class InfiniteContextManager {
14
14
  compressIfNeeded(wrapper: ChatLunaLLMChainWrapper): Promise<void>;
15
15
  private _compressMessages;
16
16
  private _isCompressedMessage;
17
+ private _isToolRelatedMessage;
18
+ private _rewriteChatHistory;
17
19
  private _splitChunksForCompression;
18
20
  private _calculateMessageTokenStats;
19
21
  private _countMessagesTokens;
20
22
  private _formatChunkForCompression;
21
23
  private _ensureInfiniteContextChain;
24
+ private _buildFinalSummary;
22
25
  }
@@ -234,6 +234,10 @@ var ChatLunaChatModel = class extends import_chat_models.BaseChatModel {
234
234
  if (maxTokenLimit != null && maxTokenLimit >= this.getModelMaxContextSize()) {
235
235
  maxTokenLimit = this.getModelMaxContextSize();
236
236
  }
237
+ let id = options?.id ?? this._options.id;
238
+ if (!id) {
239
+ id = options?.variables_hide?.["built"]?.["conversationId"];
240
+ }
237
241
  return {
238
242
  model: modelName,
239
243
  temperature: options?.temperature ?? this._options.temperature,
@@ -244,12 +248,12 @@ var ChatLunaChatModel = class extends import_chat_models.BaseChatModel {
244
248
  logitBias: options?.logitBias ?? this._options.logitBias,
245
249
  maxTokens: options?.maxTokens ?? this._options.maxTokens,
246
250
  maxTokenLimit,
247
- variables: options?.["variables_hide"] ?? {},
251
+ variables: options?.["variables_hide"] ?? options?.["variables"] ?? {},
248
252
  overrideRequestParams: options?.overrideRequestParams ?? this._options.overrideRequestParams ?? {},
249
253
  stop: options?.stop ?? this._options.stop,
250
254
  stream: options?.stream ?? this._options.stream,
251
255
  tools: options?.tools ?? this._options.tools,
252
- id: options?.id ?? this._options.id,
256
+ id,
253
257
  signal: options?.signal ?? this._options.signal,
254
258
  timeout: options?.timeout ?? this._options.timeout
255
259
  };
@@ -216,6 +216,10 @@ var ChatLunaChatModel = class extends BaseChatModel {
216
216
  if (maxTokenLimit != null && maxTokenLimit >= this.getModelMaxContextSize()) {
217
217
  maxTokenLimit = this.getModelMaxContextSize();
218
218
  }
219
+ let id = options?.id ?? this._options.id;
220
+ if (!id) {
221
+ id = options?.variables_hide?.["built"]?.["conversationId"];
222
+ }
219
223
  return {
220
224
  model: modelName,
221
225
  temperature: options?.temperature ?? this._options.temperature,
@@ -226,12 +230,12 @@ var ChatLunaChatModel = class extends BaseChatModel {
226
230
  logitBias: options?.logitBias ?? this._options.logitBias,
227
231
  maxTokens: options?.maxTokens ?? this._options.maxTokens,
228
232
  maxTokenLimit,
229
- variables: options?.["variables_hide"] ?? {},
233
+ variables: options?.["variables_hide"] ?? options?.["variables"] ?? {},
230
234
  overrideRequestParams: options?.overrideRequestParams ?? this._options.overrideRequestParams ?? {},
231
235
  stop: options?.stop ?? this._options.stop,
232
236
  stream: options?.stream ?? this._options.stream,
233
237
  tools: options?.tools ?? this._options.tools,
234
- id: options?.id ?? this._options.id,
238
+ id,
235
239
  signal: options?.signal ?? this._options.signal,
236
240
  timeout: options?.timeout ?? this._options.timeout
237
241
  };
@@ -330,6 +330,7 @@ function getSystemPromptVariables(session, config, room) {
330
330
  group_id: session.guildId ?? session.event?.guild?.id,
331
331
  group_name: session.event?.guild?.name || session.guildId,
332
332
  user_id: session.author?.user?.id ?? session.event?.user?.id ?? session.userId ?? "0",
333
+ platform: session.platform,
333
334
  user: getNotEmptyString(
334
335
  session.author?.nick,
335
336
  session.author?.name,
@@ -338,6 +339,7 @@ function getSystemPromptVariables(session, config, room) {
338
339
  ),
339
340
  built: {
340
341
  preset: room.preset,
342
+ platform: session.platform,
341
343
  conversationId: room.conversationId
342
344
  },
343
345
  noop: "",
@@ -45,9 +45,11 @@ export declare function getSystemPromptVariables(session: Session, config: Confi
45
45
  group_id: string;
46
46
  group_name: string;
47
47
  user_id: string;
48
+ platform: string;
48
49
  user: string;
49
50
  built: {
50
51
  preset: string;
52
+ platform: string;
51
53
  conversationId: string;
52
54
  };
53
55
  noop: string;
@@ -277,6 +277,7 @@ function getSystemPromptVariables(session, config, room) {
277
277
  group_id: session.guildId ?? session.event?.guild?.id,
278
278
  group_name: session.event?.guild?.name || session.guildId,
279
279
  user_id: session.author?.user?.id ?? session.event?.user?.id ?? session.userId ?? "0",
280
+ platform: session.platform,
280
281
  user: getNotEmptyString(
281
282
  session.author?.nick,
282
283
  session.author?.name,
@@ -285,6 +286,7 @@ function getSystemPromptVariables(session, config, room) {
285
286
  ),
286
287
  built: {
287
288
  preset: room.preset,
289
+ platform: session.platform,
288
290
  conversationId: room.conversationId
289
291
  },
290
292
  noop: "",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chatluna",
3
3
  "description": "chatluna for koishi",
4
- "version": "1.3.4",
4
+ "version": "1.3.6",
5
5
  "main": "lib/index.cjs",
6
6
  "module": "lib/index.mjs",
7
7
  "typings": "lib/index.d.ts",