feat: /compress 上下文压缩 + Skill 渐进式加载

This commit is contained in:
Metona Dev
2026-04-24 12:35:49 +08:00
parent fe85251738
commit 8e53a411d6
5 changed files with 220 additions and 43 deletions
+20 -10
View File
@@ -16,6 +16,7 @@ import { addToolCard, updateToolCard, clearToolCardsExternal, clearTerminalExter
import { ChatDB } from '../db/chat-db.js'; import { ChatDB } from '../db/chat-db.js';
import { OllamaAPI } from '../api/ollama.js'; import { OllamaAPI } from '../api/ollama.js';
import { runAgentLoop } from '../services/agent-engine.js'; import { runAgentLoop } from '../services/agent-engine.js';
import { estimateTokens } from '../services/context-manager.js';
import { searchMemories, buildMemoryContext, markMemoryUsed, extractMemoriesFromConversation, isMemoryEnabled } from '../services/memory-manager.js'; import { searchMemories, buildMemoryContext, markMemoryUsed, extractMemoriesFromConversation, isMemoryEnabled } from '../services/memory-manager.js';
import { showToolConfirm } from './tool-confirm-modal.js'; import { showToolConfirm } from './tool-confirm-modal.js';
import { logInfo, logStream, logError, logSuccess, logWarn } from '../services/log-service.js'; import { logInfo, logStream, logError, logSuccess, logWarn } from '../services/log-service.js';
@@ -448,7 +449,7 @@ async function handleCompress(): Promise<void> {
logInfo('上下文压缩: 开始'); logInfo('上下文压缩: 开始');
// 取中间的消息做摘要(保留首尾各 2 条) // 取中间的消息做摘要(保留首尾各 2 条,跳过已压缩的
const messages = currentSession.messages; const messages = currentSession.messages;
const keepStart = 2; const keepStart = 2;
const keepEnd = 2; const keepEnd = 2;
@@ -456,10 +457,16 @@ async function handleCompress(): Promise<void> {
const tail = messages.slice(-keepEnd); const tail = messages.slice(-keepEnd);
const middle = messages.slice(keepStart, messages.length - keepEnd); const middle = messages.slice(keepStart, messages.length - keepEnd);
const conversationText = middle.map(m => { // 过滤掉已压缩的消息,避免重复压缩
const uncompressedMiddle = middle.filter(m => !m.compressed);
if (uncompressedMiddle.length === 0) {
showToast('中间消息已全部压缩,无需再次压缩', 'info');
return;
}
const conversationText = uncompressedMiddle.map(m => {
const role = m.role === 'user' ? '用户' : 'AI'; const role = m.role === 'user' ? '用户' : 'AI';
let content = m.content || ''; let content = m.content || '';
// 截断过长内容
if (content.length > 500) content = content.slice(0, 500) + '...'; if (content.length > 500) content = content.slice(0, 500) + '...';
if (m.toolCalls?.length) content += ` [工具调用: ${m.toolCalls.map(t => t.name).join(', ')}]`; if (m.toolCalls?.length) content += ` [工具调用: ${m.toolCalls.map(t => t.name).join(', ')}]`;
return `${role}: ${content}`; return `${role}: ${content}`;
@@ -473,7 +480,7 @@ async function handleCompress(): Promise<void> {
model, model,
messages: [{ messages: [{
role: 'user', role: 'user',
content: `请将以下对话摘要为一段简洁的上下文总结。保留关键信息:讨论的主题、得出的结论、用户的偏好、未完成的任务。用中文输出,不超过 500 字。\n\n对话记录:\n${conversationText}` content: `请将以下对话摘要为一段简洁的上下文总结。保留关键信息:讨论的主题、得出的结论、用户的偏好、未完成的任务、关键工具调用结果。用中文输出,不超过 500 字。\n\n对话记录:\n${conversationText}`
}], }],
stream: true, stream: true,
think: false, think: false,
@@ -489,14 +496,17 @@ async function handleCompress(): Promise<void> {
return; return;
} }
// 构建压缩后的消息列表 // 构建压缩后的摘要消息(标记 compressed
const summaryMsg: ChatMessage = { const summaryMsg: ChatMessage = {
role: 'system', role: 'system',
content: `📋 以下是对之前对话的摘要(已压缩 ${middle.length} 条消息):\n\n${summary}`, content: `📋 以下是对之前对话的摘要(已压缩 ${uncompressedMiddle.length} 条消息):\n\n${summary}`,
timestamp: Date.now() timestamp: Date.now(),
compressed: true
}; };
currentSession.messages = [...head, summaryMsg, ...tail]; // 保留已压缩的中间消息 + 新摘要
const alreadyCompressed = middle.filter(m => m.compressed);
currentSession.messages = [...head, ...alreadyCompressed, summaryMsg, ...tail];
const db = state.get<ChatDB | null>(KEYS.DB); const db = state.get<ChatDB | null>(KEYS.DB);
if (db) await db.saveSession(currentSession); if (db) await db.saveSession(currentSession);
@@ -504,8 +514,8 @@ async function handleCompress(): Promise<void> {
clearMessagesDOM(); clearMessagesDOM();
renderMessages(); renderMessages();
logInfo(`上下文压缩完成: ${middle.length} 条 → 1 条摘要`); logInfo(`上下文压缩完成: ${uncompressedMiddle.length} 条 → 1 条摘要`);
showToast(`已压缩 ${middle.length} 条消息为摘要`, 'success'); showToast(`已压缩 ${uncompressedMiddle.length} 条消息为摘要`, 'success');
} catch (err) { } catch (err) {
logError('上下文压缩失败', (err as Error).message); logError('上下文压缩失败', (err as Error).message);
showToast(`压缩失败: ${(err as Error).message}`, 'error'); showToast(`压缩失败: ${(err as Error).message}`, 'error');
+17 -1
View File
@@ -17,7 +17,7 @@ import { showToast } from '../components/toast.js';
import { logInfo, logWarn, logError, logToolStart, logToolResult, logAgentLoop, logModelResponse } from './log-service.js'; import { logInfo, logWarn, logError, logToolStart, logToolResult, logAgentLoop, logModelResponse } from './log-service.js';
import { getWorkspaceDirPath } from '../components/workspace-panel.js'; import { getWorkspaceDirPath } from '../components/workspace-panel.js';
import { generateId } from '../utils/utils.js'; import { generateId } from '../utils/utils.js';
import { buildContext, estimateTokens } from './context-manager.js'; import { buildContext, estimateTokens, shouldAutoCompress, compressWithLLM, AUTO_COMPRESS_THRESHOLD } from './context-manager.js';
import type { import type {
OllamaMessage, OllamaMessage,
OllamaStreamChunk, OllamaStreamChunk,
@@ -497,6 +497,22 @@ export async function runAgentLoop(
messages.length = 0; messages.length = 0;
messages.push(...contextResult); messages.push(...contextResult);
// 自动压缩:当上下文 token 超过 context window 的 50% 时,调用 LLM 摘要压缩
if (shouldAutoCompress(messages, numCtx)) {
logInfo(`自动上下文压缩触发: tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))} > ${Math.floor(numCtx * AUTO_COMPRESS_THRESHOLD)} (50% of ${numCtx})`);
try {
const compressed = await compressWithLLM(messages, api, model, { abortController });
if (compressed.length < messages.length || estimateTokens(compressed.map(m => m.content || '').join('')) < estimateTokens(messages.map(m => m.content || '').join(''))) {
messages.length = 0;
messages.push(...compressed);
logSuccess(`自动上下文压缩完成: 剩余 ${messages.length} 条消息`);
}
} catch (err) {
if ((err as Error).name === 'AbortError') throw err;
logWarn('自动上下文压缩失败,继续使用当前上下文', (err as Error).message);
}
}
logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}, tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))}`); logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}, tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))}`);
// 迭代预算:从 state 读取,默认 85 // 迭代预算:从 state 读取,默认 85
+159 -19
View File
@@ -1,10 +1,11 @@
/** /**
* Context Manager - 智能上下文管理 (v4.0) * Context Manager - 智能上下文管理 (v5.0)
* 三层策略:滑动窗口 + 消息摘要压缩 + 记忆注入 * 三层策略:滑动窗口 + LLM 摘要压缩 + 记忆注入
* 支持自动压缩与手动 /compress 触发
*/ */
import type { OllamaMessage } from '../types.js'; import type { OllamaMessage, OllamaStreamChunk } from '../types.js';
import { logInfo, logWarn } from './log-service.js'; import { logInfo, logWarn, logSuccess, logError } from './log-service.js';
/** 粗略估算 token 数 */ /** 粗略估算 token 数 */
export function estimateTokens(text: string): number { export function estimateTokens(text: string): number {
@@ -19,6 +20,13 @@ export function estimateTokens(text: string): number {
return Math.ceil(chineseChars / 1.5 + otherChars / 4); return Math.ceil(chineseChars / 1.5 + otherChars / 4);
} }
/** 自动压缩阈值:当消息 token 占 context window 比例超过此值时触发自动压缩 */
export const AUTO_COMPRESS_THRESHOLD = 0.5;
/** 压缩后保留首尾消息数 */
const COMPRESS_KEEP_HEAD = 2;
const COMPRESS_KEEP_TAIL = 2;
export interface ContextBuildOptions { export interface ContextBuildOptions {
/** 滑动窗口大小(最近 N 条消息完整保留) */ /** 滑动窗口大小(最近 N 条消息完整保留) */
windowSize?: number; windowSize?: number;
@@ -41,10 +49,10 @@ const DEFAULT_OPTIONS: Required<ContextBuildOptions> = {
}; };
/** /**
* 构建发送给模型的 messages * 构建发送给模型的 messages(同步,滑动窗口)
* 三层策略: * 三层策略:
* a. 滑动窗口:最近 N 条消息完整保留 * a. 滑动窗口:最近 N 条消息完整保留
* b. 更早的消息:每 N 条压缩为一段摘要 * b. 更早的消息:每 N 条压缩为一段摘要(快速文本截取)
* c. 系统 prompt 注入记忆上下文 * c. 系统 prompt 注入记忆上下文
*/ */
export function buildContext( export function buildContext(
@@ -64,9 +72,9 @@ export function buildContext(
} }
// 提取已有的 system 消息 // 提取已有的 system 消息
const existingSystem = allMessages.find(m => m.role === 'system'); const existingSystem = allMessages.filter(m => m.role === 'system');
if (existingSystem) { for (const sys of existingSystem) {
systemContent += existingSystem.content; systemContent += sys.content + '\n';
} }
if (systemContent.trim()) { if (systemContent.trim()) {
@@ -77,7 +85,6 @@ export function buildContext(
const nonSystemMessages = allMessages.filter(m => m.role !== 'system'); const nonSystemMessages = allMessages.filter(m => m.role !== 'system');
if (nonSystemMessages.length <= opts.windowSize) { if (nonSystemMessages.length <= opts.windowSize) {
// 消息不多,全部保留
result.push(...nonSystemMessages); result.push(...nonSystemMessages);
return result; return result;
} }
@@ -85,11 +92,21 @@ export function buildContext(
// 滑动窗口:最近 N 条 // 滑动窗口:最近 N 条
const recentMessages = nonSystemMessages.slice(-opts.windowSize); const recentMessages = nonSystemMessages.slice(-opts.windowSize);
// 更早的消息:压缩摘要 // 更早的消息:压缩标记的保留原样,未压缩的做快速摘要
const olderMessages = nonSystemMessages.slice(0, -opts.windowSize); const olderMessages = nonSystemMessages.slice(0, -opts.windowSize);
const summaries = summarizeOlderMessages(olderMessages, opts.summaryBatchSize); const compressedMsgs = olderMessages.filter(m => m.compressed);
const uncompressedMsgs = olderMessages.filter(m => !m.compressed);
result.push(...summaries, ...recentMessages); // 已压缩的消息直接保留
result.push(...compressedMsgs);
// 未压缩的消息做快速摘要
if (uncompressedMsgs.length > 0) {
const summaries = summarizeOlderMessages(uncompressedMsgs, opts.summaryBatchSize);
result.push(...summaries);
}
result.push(...recentMessages);
// Token 估算和裁剪 // Token 估算和裁剪
const trimmed = trimByTokenLimit(result, opts.maxTokens); const trimmed = trimByTokenLimit(result, opts.maxTokens);
@@ -101,7 +118,132 @@ export function buildContext(
} }
/** /**
* 将较早的消息每 batchSize 条压缩为一段摘要 * 判断是否需要自动压缩
* 当总 token 数超过 context window 的 AUTO_COMPRESS_THRESHOLD 比例时返回 true
*/
export function shouldAutoCompress(messages: OllamaMessage[], numCtx: number): boolean {
const totalTokens = estimateTokens(messages.map(m => m.content || '').join(''));
const threshold = numCtx * AUTO_COMPRESS_THRESHOLD;
return totalTokens > threshold;
}
/**
* LLM 摘要压缩:调用模型对中间消息生成摘要
* 保留首尾各 keepHead/keepTail 条消息,中间用 LLM 摘要替换
*
* @returns 压缩后的消息列表(包含 compressed 标记的摘要消息)
*/
export async function compressWithLLM(
messages: OllamaMessage[],
api: { chatStream: (params: Record<string, unknown>, onChunk: (chunk: OllamaStreamChunk) => void, ac?: AbortController) => Promise<void> },
model: string,
options: {
keepHead?: number;
keepTail?: number;
maxSummaryTokens?: number;
abortController?: AbortController;
} = {}
): Promise<OllamaMessage[]> {
const keepHead = options.keepHead ?? COMPRESS_KEEP_HEAD;
const keepTail = options.keepTail ?? COMPRESS_KEEP_TAIL;
const maxSummaryTokens = options.maxSummaryTokens ?? 500;
// 分离 system 和非 system 消息
const systemMsgs = messages.filter(m => m.role === 'system');
const nonSystemMsgs = messages.filter(m => m.role !== 'system');
if (nonSystemMsgs.length <= keepHead + keepTail + 2) {
logInfo('上下文压缩: 消息太少,跳过压缩');
return messages;
}
const head = nonSystemMsgs.slice(0, keepHead);
const tail = nonSystemMsgs.slice(-keepTail);
const middle = nonSystemMsgs.slice(keepHead, nonSystemMsgs.length - keepTail);
// 过滤掉已经压缩过的消息(避免重复压缩)
const uncompressedMiddle = middle.filter(m => !m.compressed);
if (uncompressedMiddle.length === 0) {
logInfo('上下文压缩: 中间消息已全部压缩,跳过');
return messages;
}
// 构建对话文本
const conversationText = uncompressedMiddle.map(m => {
const role = m.role === 'user' ? '用户' : 'AI';
let content = m.content || '';
if (content.length > 500) content = content.slice(0, 500) + '...';
if (m.tool_calls?.length) {
const toolNames = m.tool_calls.map(t => t.function.name).join(', ');
content += ` [工具调用: ${toolNames}]`;
}
return `${role}: ${content}`;
}).join('\n');
logInfo(`上下文压缩: 开始 LLM 摘要,${uncompressedMiddle.length} 条消息待压缩`);
let summary = '';
try {
await api.chatStream(
{
model,
messages: [{
role: 'user',
content: `请将以下对话摘要为一段简洁的上下文总结。保留关键信息:讨论的主题、得出的结论、用户的偏好、未完成的任务、关键工具调用结果。用中文输出,不超过 ${maxSummaryTokens} 字。\n\n对话记录:\n${conversationText}`
}],
stream: true,
think: false,
options: { num_ctx: 8192, temperature: 0.3 }
},
(chunk: OllamaStreamChunk) => {
if (chunk.message?.content) {
summary += chunk.message.content;
}
},
options.abortController
);
} catch (err) {
if ((err as Error).name === 'AbortError') {
logWarn('上下文压缩: LLM 调用被中止');
return messages;
}
logError('上下文压缩: LLM 调用失败', (err as Error).message);
return messages;
}
if (!summary.trim()) {
logWarn('上下文压缩: 模型未返回摘要内容');
return messages;
}
// 构建压缩后的摘要消息
const summaryMsg: OllamaMessage = {
role: 'system',
content: `📋 以下是对之前对话的摘要(已压缩 ${uncompressedMiddle.length} 条消息):\n\n${summary}`,
compressed: true
};
// 保留已压缩的中间消息 + 新摘要
const alreadyCompressed = middle.filter(m => m.compressed);
const result: OllamaMessage[] = [
...systemMsgs,
...head,
...alreadyCompressed,
summaryMsg,
...tail
];
const beforeTokens = estimateTokens(messages.map(m => m.content || '').join(''));
const afterTokens = estimateTokens(result.map(m => m.content || '').join(''));
logSuccess(`上下文压缩完成: ${messages.length} 条 → ${result.length} 条, tokens: ${beforeTokens}${afterTokens}`);
return result;
}
/**
* 将较早的消息每 batchSize 条压缩为一段摘要(快速文本截取,不调用 LLM)
*/ */
function summarizeOlderMessages(messages: OllamaMessage[], batchSize: number): OllamaMessage[] { function summarizeOlderMessages(messages: OllamaMessage[], batchSize: number): OllamaMessage[] {
const summaries: OllamaMessage[] = []; const summaries: OllamaMessage[] = [];
@@ -111,7 +253,8 @@ function summarizeOlderMessages(messages: OllamaMessage[], batchSize: number): O
const summary = createQuickSummary(batch); const summary = createQuickSummary(batch);
summaries.push({ summaries.push({
role: 'system', role: 'system',
content: `【更早的对话摘要(第 ${Math.floor(i / batchSize) + 1} 部分)】\n${summary}` content: `【更早的对话摘要(第 ${Math.floor(i / batchSize) + 1} 部分)】\n${summary}`,
compressed: true
}); });
} }
@@ -127,12 +270,10 @@ function createQuickSummary(messages: OllamaMessage[]): string {
for (const msg of messages) { for (const msg of messages) {
const role = msg.role === 'user' ? '用户' : 'AI'; const role = msg.role === 'user' ? '用户' : 'AI';
const content = msg.content || ''; const content = msg.content || '';
// 取前 100 字符作为摘要
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content; const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
if (preview.trim()) { if (preview.trim()) {
parts.push(`${role}: ${preview}`); parts.push(`${role}: ${preview}`);
} }
// 如果有工具调用,记录工具名
if (msg.tool_calls?.length) { if (msg.tool_calls?.length) {
const toolNames = msg.tool_calls.map(t => t.function.name).join(', '); const toolNames = msg.tool_calls.map(t => t.function.name).join(', ');
parts.push(` [工具: ${toolNames}]`); parts.push(` [工具: ${toolNames}]`);
@@ -149,7 +290,6 @@ function trimByTokenLimit(messages: OllamaMessage[], maxTokens: number): OllamaM
let totalTokens = 0; let totalTokens = 0;
const result: OllamaMessage[] = []; const result: OllamaMessage[] = [];
// 从后往前添加(优先保留最近的消息)
for (let i = messages.length - 1; i >= 0; i--) { for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i]; const msg = messages[i];
const msgTokens = estimateTokens(msg.content || '') + const msgTokens = estimateTokens(msg.content || '') +
@@ -164,7 +304,7 @@ function trimByTokenLimit(messages: OllamaMessage[], maxTokens: number): OllamaM
} }
if (totalTokens + msgTokens > maxTokens && result.length > 2) { if (totalTokens + msgTokens > maxTokens && result.length > 2) {
break; // 已到限制,保留至少 system + 1 条消息 break;
} }
totalTokens += msgTokens; totalTokens += msgTokens;
+19 -12
View File
@@ -13,6 +13,8 @@ export interface Skill {
id: string; id: string;
name: string; name: string;
description: string; description: string;
/** Level 0 简短摘要(~100 tokens),用于索引展示 */
summary: string;
trigger_keywords: string | null; trigger_keywords: string | null;
tool_chain: string; // JSON: ToolChainStep[] tool_chain: string; // JSON: ToolChainStep[]
success_count: number; success_count: number;
@@ -83,10 +85,13 @@ export async function extractSkillsFromToolRecords(
const name = generateSkillName(chain, userMessage); const name = generateSkillName(chain, userMessage);
const description = generateSkillDescription(chain, sessionTitle); const description = generateSkillDescription(chain, sessionTitle);
const summary = generateSkillSummary(chain, userMessage);
const skill: Skill = { const skill: Skill = {
id: `skill_${generateId()}`, id: `skill_${generateId()}`,
name, name,
description, description,
summary,
trigger_keywords: keywords, trigger_keywords: keywords,
tool_chain: JSON.stringify(chain), tool_chain: JSON.stringify(chain),
success_count: 1, success_count: 1,
@@ -157,28 +162,21 @@ export async function matchSkills(userMessage: string, limit = 3): Promise<Skill
} }
/** /**
* 从匹配的技能构建 system prompt 上下文 * 从匹配的技能构建 system prompt 上下文(Level 0:仅名称 + 描述索引)
* 完整工具链通过 skill_view 工具按需加载(Level 1
*/ */
export function buildSkillContext(skills: Skill[]): string { export function buildSkillContext(skills: Skill[]): string {
if (skills.length === 0) return ''; if (skills.length === 0) return '';
const parts = skills.map(skill => { const parts = skills.map(skill => {
let chain: ToolChainStep[] = [];
try { chain = JSON.parse(skill.tool_chain); } catch { return ''; }
const steps = chain.map((s, i) =>
` ${i + 1}. ${s.name}${s.description}${s.args_hint ? ` — 参数: ${s.args_hint}` : ''}`
).join('\n');
const total = skill.success_count + skill.fail_count; const total = skill.success_count + skill.fail_count;
const rate = total > 0 ? Math.round(skill.success_count / total * 100) : 100; const rate = total > 0 ? Math.round(skill.success_count / total * 100) : 100;
return `${skill.name}${skill.description}(成功率 ${rate}%`;
return `【技能】${skill.name}\n${skill.description}\n成功执行 ${skill.success_count} 次(成功率 ${rate}%\n工具链:\n${steps}`;
}).filter(Boolean); }).filter(Boolean);
if (parts.length === 0) return ''; if (parts.length === 0) return '';
return `【可用技能提示 — 以下技能曾成功完成过类似任务,可作为参考\n\n${parts.join('\n\n')}\n\n提示:如果当前任务与某个技能匹配,可以参考其工具链步骤,但根据实际情况调整参数`; return `【可用技能 — 以下技能曾成功完成过类似任务】\n${parts.join('\n')}\n\n提示:使用 skill_view 工具查看某个技能的完整工具链和参数提示`;
} }
// ── 辅助函数 ── // ── 辅助函数 ──
@@ -269,12 +267,20 @@ function generateSkillDescription(chain: ToolChainStep[], sessionTitle: string):
return `从会话「${sessionTitle}」中提取。步骤:${steps}`; return `从会话「${sessionTitle}」中提取。步骤:${steps}`;
} }
/** 生成技能简短摘要(Level 0~100 tokens */
function generateSkillSummary(chain: ToolChainStep[], userMessage: string): string {
const toolNames = chain.map(s => s.name).join(' → ');
const msgPreview = userMessage.slice(0, 30).replace(/[\r\n]/g, ' ');
return `工具链: ${toolNames}。场景: ${msgPreview}`;
}
// ── 渐进式技能加载 ── // ── 渐进式技能加载 ──
/** 列出所有技能(渐进式 Level 0:只返回名称、描述、成功率) */ /** 列出所有技能(渐进式 Level 0:只返回名称、描述摘要、成功率) */
export async function listSkills(): Promise<Array<{ export async function listSkills(): Promise<Array<{
name: string; name: string;
description: string; description: string;
summary: string;
success_rate: string; success_rate: string;
chain_preview: string; chain_preview: string;
}>> { }>> {
@@ -292,6 +298,7 @@ export async function listSkills(): Promise<Array<{
return { return {
name: skill.name, name: skill.name,
description: skill.description, description: skill.description,
summary: skill.summary || skill.description,
success_rate: `${rate}%`, success_rate: `${rate}%`,
chain_preview: chainPreview chain_preview: chainPreview
}; };
+5 -1
View File
@@ -8,6 +8,8 @@ export interface OllamaMessage {
reasoning_content?: string; reasoning_content?: string;
tool_calls?: ToolCall[]; tool_calls?: ToolCall[];
tool_name?: string; tool_name?: string;
/** 标记此消息为 LLM 压缩摘要生成 */
compressed?: boolean;
} }
export interface OllamaChatParams { export interface OllamaChatParams {
@@ -94,7 +96,7 @@ export interface FileContent {
} }
export interface ChatMessage { export interface ChatMessage {
role: 'user' | 'assistant'; role: 'user' | 'assistant' | 'system';
content: string; content: string;
timestamp: number; timestamp: number;
model?: string; model?: string;
@@ -107,6 +109,8 @@ export interface ChatMessage {
_fileContents?: FileContent[]; _fileContents?: FileContent[];
stopped?: boolean; stopped?: boolean;
toolCalls?: ToolCallRecord[]; toolCalls?: ToolCallRecord[];
/** 标记此消息为 LLM 压缩摘要生成 */
compressed?: boolean;
} }
export interface ChatSession { export interface ChatSession {