5e32375f0e
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
308 lines
9.0 KiB
TypeScript
308 lines
9.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { prune, filterCompacted } from '../../../src/context/prune.js';
|
|
import {
|
|
COMPACTED_PLACEHOLDER,
|
|
SUMMARY_MARKER,
|
|
COMPACTED_MARKER,
|
|
type CompressionConfig,
|
|
} from '../../../src/context/types.js';
|
|
import type { ModelMessage } from 'ai';
|
|
|
|
// 创建测试用的工具结果消息
|
|
function createToolResultMessage(
|
|
toolCallId: string,
|
|
result: unknown,
|
|
compacted = false
|
|
): ModelMessage {
|
|
const content: Record<string, unknown> = {
|
|
type: 'tool-result',
|
|
toolCallId,
|
|
toolName: 'test_tool',
|
|
result,
|
|
};
|
|
|
|
if (compacted) {
|
|
content[COMPACTED_MARKER] = {
|
|
compactedAt: Date.now(),
|
|
originalSize: 100,
|
|
};
|
|
}
|
|
|
|
return {
|
|
role: 'tool',
|
|
content: [content],
|
|
} as ModelMessage;
|
|
}
|
|
|
|
// 创建测试用的工具调用消息
|
|
function createToolCallMessage(toolCallId: string): ModelMessage {
|
|
return {
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'tool-call',
|
|
toolCallId,
|
|
toolName: 'test_tool',
|
|
args: { param: 'value' },
|
|
},
|
|
],
|
|
} as ModelMessage;
|
|
}
|
|
|
|
// 创建用户消息
|
|
function createUserMessage(content: string): ModelMessage {
|
|
return { role: 'user', content };
|
|
}
|
|
|
|
// 创建助手消息
|
|
function createAssistantMessage(content: string): ModelMessage {
|
|
return { role: 'assistant', content };
|
|
}
|
|
|
|
// 创建摘要消息
|
|
function createSummaryMessage(): ModelMessage {
|
|
return {
|
|
role: 'assistant',
|
|
content: `${SUMMARY_MARKER}\n## 对话摘要\n\n这是一个摘要\n${SUMMARY_MARKER}`,
|
|
};
|
|
}
|
|
|
|
// 创建大工具结果(指定 token 数)
|
|
function createLargeToolResult(toolCallId: string, sizeInChars: number): ModelMessage {
|
|
// 约 4 字符/token(英文)
|
|
const content = 'a'.repeat(sizeInChars);
|
|
return createToolResultMessage(toolCallId, { output: content });
|
|
}
|
|
|
|
describe('prune - 消息裁剪策略', () => {
|
|
// 测试配置:小值便于测试
|
|
const testConfig: CompressionConfig = {
|
|
contextLimit: 1000,
|
|
outputReserve: 100,
|
|
pruneProtect: 200, // 保护最近 200 tokens
|
|
pruneMinimum: 50, // 至少释放 50 tokens 才执行
|
|
overflowThreshold: 0.85,
|
|
};
|
|
|
|
describe('基本裁剪行为', () => {
|
|
it('空消息数组不裁剪', () => {
|
|
const result = prune([], testConfig);
|
|
|
|
expect(result.messages).toHaveLength(0);
|
|
expect(result.freedTokens).toBe(0);
|
|
});
|
|
|
|
it('只有用户消息不裁剪', () => {
|
|
const messages: ModelMessage[] = [
|
|
createUserMessage('Hello'),
|
|
createUserMessage('How are you?'),
|
|
];
|
|
|
|
const result = prune(messages, testConfig);
|
|
|
|
expect(result.messages).toEqual(messages);
|
|
expect(result.freedTokens).toBe(0);
|
|
});
|
|
|
|
it('保护范围内的工具结果不裁剪', () => {
|
|
// 创建小的工具结果(在保护范围内)
|
|
const messages: ModelMessage[] = [
|
|
createUserMessage('Use tool'),
|
|
createToolCallMessage('call_1'),
|
|
createToolResultMessage('call_1', { output: 'small result' }),
|
|
];
|
|
|
|
const result = prune(messages, testConfig);
|
|
|
|
// 不应该裁剪
|
|
expect(result.freedTokens).toBe(0);
|
|
});
|
|
|
|
it('裁剪超出保护范围的工具结果', () => {
|
|
// 创建多个工具结果,超出保护范围
|
|
const messages: ModelMessage[] = [
|
|
createUserMessage('Task 1'),
|
|
createToolCallMessage('call_1'),
|
|
createLargeToolResult('call_1', 1000), // ~250 tokens, 超出 pruneProtect
|
|
createUserMessage('Task 2'),
|
|
createToolCallMessage('call_2'),
|
|
createLargeToolResult('call_2', 400), // ~100 tokens
|
|
createUserMessage('Task 3'),
|
|
createToolCallMessage('call_3'),
|
|
createToolResultMessage('call_3', { output: 'recent' }), // 最近的,在保护范围内
|
|
];
|
|
|
|
const result = prune(messages, testConfig);
|
|
|
|
// 应该裁剪旧的大工具结果
|
|
expect(result.freedTokens).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('摘要消息边界', () => {
|
|
it('遇到摘要消息停止裁剪', () => {
|
|
const messages: ModelMessage[] = [
|
|
createSummaryMessage(), // 摘要消息
|
|
createUserMessage('After summary'),
|
|
createToolCallMessage('call_1'),
|
|
createLargeToolResult('call_1', 2000), // 大结果
|
|
];
|
|
|
|
const result = prune(messages, testConfig);
|
|
|
|
// 因为遇到摘要消息,不会继续向前裁剪
|
|
// 但是摘要后面的大工具结果如果超出保护范围仍会被裁剪
|
|
expect(result.messages[0]).toEqual(messages[0]); // 摘要保留
|
|
});
|
|
|
|
it('摘要消息前的内容不裁剪', () => {
|
|
const messages: ModelMessage[] = [
|
|
createUserMessage('Before summary'),
|
|
createToolCallMessage('call_old'),
|
|
createLargeToolResult('call_old', 2000), // 摘要前的大结果
|
|
createSummaryMessage(),
|
|
createUserMessage('After summary'),
|
|
];
|
|
|
|
const result = prune(messages, testConfig);
|
|
|
|
// 摘要前的内容因为遇到摘要边界停止
|
|
expect(result.messages.length).toBe(messages.length);
|
|
});
|
|
});
|
|
|
|
describe('已压缩内容处理', () => {
|
|
it('遇到已压缩的工具结果停止', () => {
|
|
const messages: ModelMessage[] = [
|
|
createUserMessage('Task 1'),
|
|
createToolCallMessage('call_1'),
|
|
createToolResultMessage('call_1', COMPACTED_PLACEHOLDER, true), // 已压缩
|
|
createUserMessage('Task 2'),
|
|
createToolCallMessage('call_2'),
|
|
createLargeToolResult('call_2', 1000), // 新的大结果
|
|
];
|
|
|
|
const result = prune(messages, testConfig);
|
|
|
|
// 遇到已压缩的结果应该停止继续向前
|
|
expect(result.messages).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('最小裁剪量检查', () => {
|
|
it('释放量不足最小值时不执行裁剪', () => {
|
|
// 使用较大的 pruneMinimum
|
|
const strictConfig: CompressionConfig = {
|
|
...testConfig,
|
|
pruneMinimum: 10000, // 要求至少释放 10000 tokens
|
|
};
|
|
|
|
const messages: ModelMessage[] = [
|
|
createUserMessage('Task'),
|
|
createToolCallMessage('call_1'),
|
|
createLargeToolResult('call_1', 400), // 只有 ~100 tokens
|
|
];
|
|
|
|
const result = prune(messages, strictConfig);
|
|
|
|
expect(result.freedTokens).toBe(0);
|
|
expect(result.messages).toEqual(messages);
|
|
});
|
|
});
|
|
|
|
describe('深拷贝验证', () => {
|
|
it('不修改原消息数组', () => {
|
|
const messages: ModelMessage[] = [
|
|
createUserMessage('Task'),
|
|
createToolCallMessage('call_1'),
|
|
createLargeToolResult('call_1', 2000),
|
|
createUserMessage('Recent'),
|
|
];
|
|
|
|
const originalLength = messages.length;
|
|
const originalFirstMessage = { ...messages[0] };
|
|
|
|
prune(messages, testConfig);
|
|
|
|
expect(messages.length).toBe(originalLength);
|
|
expect(messages[0]).toEqual(originalFirstMessage);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('filterCompacted - 过滤已压缩内容', () => {
|
|
it('不修改普通消息', () => {
|
|
const messages: ModelMessage[] = [
|
|
createUserMessage('Hello'),
|
|
createAssistantMessage('Hi there'),
|
|
];
|
|
|
|
const result = filterCompacted(messages);
|
|
|
|
expect(result).toEqual(messages);
|
|
});
|
|
|
|
it('不修改未压缩的工具结果', () => {
|
|
const messages: ModelMessage[] = [
|
|
createToolResultMessage('call_1', { output: 'normal result' }),
|
|
];
|
|
|
|
const result = filterCompacted(messages);
|
|
|
|
expect(result).toEqual(messages);
|
|
});
|
|
|
|
it('将已压缩工具结果替换为占位符', () => {
|
|
const messages: ModelMessage[] = [
|
|
createToolResultMessage('call_1', 'original', true), // 已压缩
|
|
];
|
|
|
|
const result = filterCompacted(messages);
|
|
|
|
const content = result[0].content as { result: unknown }[];
|
|
expect(content[0].result).toBe(COMPACTED_PLACEHOLDER);
|
|
});
|
|
|
|
it('混合内容正确处理', () => {
|
|
const messages: ModelMessage[] = [
|
|
createUserMessage('Task'),
|
|
createToolResultMessage('call_1', 'original', true), // 已压缩
|
|
createToolResultMessage('call_2', { output: 'normal' }), // 未压缩
|
|
createAssistantMessage('Done'),
|
|
];
|
|
|
|
const result = filterCompacted(messages);
|
|
|
|
expect(result).toHaveLength(4);
|
|
// 第一个工具结果应该被替换
|
|
const compactedContent = result[1].content as { result: unknown }[];
|
|
expect(compactedContent[0].result).toBe(COMPACTED_PLACEHOLDER);
|
|
// 第二个工具结果应该保持不变
|
|
const normalContent = result[2].content as { result: unknown }[];
|
|
expect(normalContent[0].result).toEqual({ output: 'normal' });
|
|
});
|
|
|
|
it('保留消息的其他属性', () => {
|
|
const messages: ModelMessage[] = [
|
|
{
|
|
role: 'tool',
|
|
content: [
|
|
{
|
|
type: 'tool-result',
|
|
toolCallId: 'call_1',
|
|
toolName: 'my_tool',
|
|
result: 'data',
|
|
[COMPACTED_MARKER]: { compactedAt: 123, originalSize: 100 },
|
|
},
|
|
],
|
|
} as ModelMessage,
|
|
];
|
|
|
|
const result = filterCompacted(messages);
|
|
|
|
const content = result[0].content as { toolCallId: string; toolName: string }[];
|
|
expect(content[0].toolCallId).toBe('call_1');
|
|
expect(content[0].toolName).toBe('my_tool');
|
|
});
|
|
});
|