@dommaker/harness 1.5.0 → 1.6.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.
Files changed (208) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +0 -2
  3. package/dist/cli/commands/definitions.d.ts.map +1 -1
  4. package/dist/cli/commands/definitions.js +1 -32
  5. package/dist/cli/commands/definitions.js.map +1 -1
  6. package/dist/cli/commands/passes-gate.d.ts +0 -2
  7. package/dist/cli/commands/passes-gate.d.ts.map +1 -1
  8. package/dist/cli/commands/passes-gate.js +0 -1
  9. package/dist/cli/commands/passes-gate.js.map +1 -1
  10. package/dist/cli/commands/performance.d.ts +0 -4
  11. package/dist/cli/commands/performance.d.ts.map +1 -1
  12. package/dist/cli/commands/performance.js +10 -20
  13. package/dist/cli/commands/performance.js.map +1 -1
  14. package/dist/cli/commands/report.d.ts +1 -1
  15. package/dist/cli/commands/report.d.ts.map +1 -1
  16. package/dist/cli/commands/report.js +2 -55
  17. package/dist/cli/commands/report.js.map +1 -1
  18. package/dist/cli/commands/status.d.ts.map +1 -1
  19. package/dist/cli/commands/status.js +4 -8
  20. package/dist/cli/commands/status.js.map +1 -1
  21. package/dist/context/index.d.ts +2 -5
  22. package/dist/context/index.d.ts.map +1 -1
  23. package/dist/context/index.js +2 -8
  24. package/dist/context/index.js.map +1 -1
  25. package/dist/context/token-budget.d.ts +0 -29
  26. package/dist/context/token-budget.d.ts.map +1 -1
  27. package/dist/context/token-budget.js +1 -65
  28. package/dist/context/token-budget.js.map +1 -1
  29. package/dist/context/types.d.ts +0 -1
  30. package/dist/context/types.d.ts.map +1 -1
  31. package/dist/context/types.js +0 -9
  32. package/dist/context/types.js.map +1 -1
  33. package/dist/core/constraints/checker.d.ts +14 -0
  34. package/dist/core/constraints/checker.d.ts.map +1 -1
  35. package/dist/core/constraints/checker.js +22 -2
  36. package/dist/core/constraints/checker.js.map +1 -1
  37. package/dist/core/constraints/git-evidence.d.ts +6 -3
  38. package/dist/core/constraints/git-evidence.d.ts.map +1 -1
  39. package/dist/core/constraints/git-evidence.js +6 -3
  40. package/dist/core/constraints/git-evidence.js.map +1 -1
  41. package/dist/core/index.d.ts +1 -2
  42. package/dist/core/index.d.ts.map +1 -1
  43. package/dist/core/index.js +1 -9
  44. package/dist/core/index.js.map +1 -1
  45. package/dist/core/spec/validator.d.ts +2 -0
  46. package/dist/core/spec/validator.d.ts.map +1 -1
  47. package/dist/core/spec/validator.js +9 -11
  48. package/dist/core/spec/validator.js.map +1 -1
  49. package/dist/core/validators/index.d.ts +1 -1
  50. package/dist/core/validators/index.d.ts.map +1 -1
  51. package/dist/core/validators/passes-gate.d.ts +1 -15
  52. package/dist/core/validators/passes-gate.d.ts.map +1 -1
  53. package/dist/core/validators/passes-gate.js +0 -125
  54. package/dist/core/validators/passes-gate.js.map +1 -1
  55. package/dist/failure/index.d.ts +0 -1
  56. package/dist/failure/index.d.ts.map +1 -1
  57. package/dist/failure/index.js +1 -7
  58. package/dist/failure/index.js.map +1 -1
  59. package/dist/gates/definitions.d.ts.map +1 -1
  60. package/dist/gates/definitions.js +0 -4
  61. package/dist/gates/definitions.js.map +1 -1
  62. package/dist/gates/performance.d.ts +7 -22
  63. package/dist/gates/performance.d.ts.map +1 -1
  64. package/dist/gates/performance.js +7 -89
  65. package/dist/gates/performance.js.map +1 -1
  66. package/dist/gates/types.d.ts +2 -8
  67. package/dist/gates/types.d.ts.map +1 -1
  68. package/dist/index.d.ts +6 -8
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4 -22
  71. package/dist/index.js.map +1 -1
  72. package/dist/knowledge/index.d.ts +0 -1
  73. package/dist/knowledge/index.d.ts.map +1 -1
  74. package/dist/knowledge/index.js +1 -3
  75. package/dist/knowledge/index.js.map +1 -1
  76. package/dist/monitoring/trace-analyzer.d.ts +25 -10
  77. package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
  78. package/dist/monitoring/trace-analyzer.js +147 -116
  79. package/dist/monitoring/trace-analyzer.js.map +1 -1
  80. package/dist/types/passes-gate.d.ts +0 -19
  81. package/dist/types/passes-gate.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/src/__tests__/passes-gate-check.test.ts +20 -4
  84. package/src/__tests__/passes-gate.test.ts +5 -99
  85. package/src/__tests__/performance-gate.test.ts +3 -11
  86. package/src/__tests__/public-exports.test.ts +46 -16
  87. package/src/__tests__/trace-analyzer.test.ts +52 -2
  88. package/src/cli/commands/CONTEXT.md +3 -2
  89. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +70 -0
  90. package/src/cli/commands/__tests__/passes-gate.test.ts +7 -3
  91. package/src/cli/commands/__tests__/performance.test.ts +5 -16
  92. package/src/cli/commands/__tests__/registry.test.ts +46 -3
  93. package/src/cli/commands/__tests__/report-extra.test.ts +0 -19
  94. package/src/cli/commands/__tests__/report-false-green.test.ts +46 -0
  95. package/src/cli/commands/__tests__/report.test.ts +0 -20
  96. package/src/cli/commands/__tests__/status-analysis-wiring.test.ts +68 -0
  97. package/src/cli/commands/__tests__/status-extra.test.ts +63 -116
  98. package/src/cli/commands/__tests__/status.test.ts +66 -148
  99. package/src/cli/commands/definitions.ts +1 -32
  100. package/src/cli/commands/passes-gate.ts +0 -3
  101. package/src/cli/commands/performance.ts +13 -27
  102. package/src/cli/commands/report.ts +3 -61
  103. package/src/cli/commands/status.ts +5 -10
  104. package/src/context/CONTEXT.md +2 -3
  105. package/src/context/__tests__/token-budget.test.ts +1 -81
  106. package/src/context/index.ts +2 -7
  107. package/src/context/token-budget.ts +0 -73
  108. package/src/context/types.ts +0 -9
  109. package/src/core/CONTEXT.md +5 -6
  110. package/src/core/constraints/__tests__/collect-constraints.test.ts +66 -0
  111. package/src/core/constraints/checker.ts +33 -2
  112. package/src/core/constraints/git-evidence.ts +6 -3
  113. package/src/core/index.ts +0 -11
  114. package/src/core/spec/__tests__/validator.test.ts +24 -0
  115. package/src/core/spec/validator.ts +9 -10
  116. package/src/core/validators/__tests__/passes-gate.test.ts +2 -9
  117. package/src/core/validators/index.ts +0 -1
  118. package/src/core/validators/passes-gate.ts +1 -149
  119. package/src/failure/CONTEXT.md +8 -6
  120. package/src/failure/index.ts +0 -10
  121. package/src/gates/CONTEXT.md +3 -2
  122. package/src/gates/__tests__/performance.test.ts +6 -74
  123. package/src/gates/definitions.ts +0 -4
  124. package/src/gates/performance.ts +9 -111
  125. package/src/gates/types.ts +2 -8
  126. package/src/index.ts +1 -36
  127. package/src/knowledge/CONTEXT.md +1 -2
  128. package/src/knowledge/index.ts +0 -1
  129. package/src/monitoring/CONTEXT.md +2 -2
  130. package/src/monitoring/trace-analyzer.ts +160 -126
  131. package/src/types/passes-gate.ts +0 -26
  132. package/src/utils/__tests__/jsonl-skip-disposition.test.ts +0 -1
  133. package/dist/cli/commands/analyze-sessions.d.ts +0 -20
  134. package/dist/cli/commands/analyze-sessions.d.ts.map +0 -1
  135. package/dist/cli/commands/analyze-sessions.js +0 -290
  136. package/dist/cli/commands/analyze-sessions.js.map +0 -1
  137. package/dist/cli/commands/update-user-model.d.ts +0 -30
  138. package/dist/cli/commands/update-user-model.d.ts.map +0 -1
  139. package/dist/cli/commands/update-user-model.js +0 -406
  140. package/dist/cli/commands/update-user-model.js.map +0 -1
  141. package/dist/cli/session-mining/corrections.d.ts +0 -17
  142. package/dist/cli/session-mining/corrections.d.ts.map +0 -1
  143. package/dist/cli/session-mining/corrections.js +0 -58
  144. package/dist/cli/session-mining/corrections.js.map +0 -1
  145. package/dist/cli/session-mining/index.d.ts +0 -13
  146. package/dist/cli/session-mining/index.d.ts.map +0 -1
  147. package/dist/cli/session-mining/index.js +0 -29
  148. package/dist/cli/session-mining/index.js.map +0 -1
  149. package/dist/cli/session-mining/text.d.ts +0 -31
  150. package/dist/cli/session-mining/text.d.ts.map +0 -1
  151. package/dist/cli/session-mining/text.js +0 -126
  152. package/dist/cli/session-mining/text.js.map +0 -1
  153. package/dist/cli/session-mining/transcript.d.ts +0 -34
  154. package/dist/cli/session-mining/transcript.d.ts.map +0 -1
  155. package/dist/cli/session-mining/transcript.js +0 -130
  156. package/dist/cli/session-mining/transcript.js.map +0 -1
  157. package/dist/context/compaction.d.ts +0 -57
  158. package/dist/context/compaction.d.ts.map +0 -1
  159. package/dist/context/compaction.js +0 -165
  160. package/dist/context/compaction.js.map +0 -1
  161. package/dist/core/session/clean-state.d.ts +0 -17
  162. package/dist/core/session/clean-state.d.ts.map +0 -1
  163. package/dist/core/session/clean-state.js +0 -192
  164. package/dist/core/session/clean-state.js.map +0 -1
  165. package/dist/core/session/index.d.ts +0 -6
  166. package/dist/core/session/index.d.ts.map +0 -1
  167. package/dist/core/session/index.js +0 -15
  168. package/dist/core/session/index.js.map +0 -1
  169. package/dist/core/session/startup.d.ts +0 -47
  170. package/dist/core/session/startup.d.ts.map +0 -1
  171. package/dist/core/session/startup.js +0 -263
  172. package/dist/core/session/startup.js.map +0 -1
  173. package/dist/failure/constraint-handler.d.ts +0 -83
  174. package/dist/failure/constraint-handler.d.ts.map +0 -1
  175. package/dist/failure/constraint-handler.js +0 -138
  176. package/dist/failure/constraint-handler.js.map +0 -1
  177. package/dist/knowledge/lifecycle-hooks.d.ts +0 -62
  178. package/dist/knowledge/lifecycle-hooks.d.ts.map +0 -1
  179. package/dist/knowledge/lifecycle-hooks.js +0 -143
  180. package/dist/knowledge/lifecycle-hooks.js.map +0 -1
  181. package/dist/types/session.d.ts +0 -113
  182. package/dist/types/session.d.ts.map +0 -1
  183. package/dist/types/session.js +0 -6
  184. package/dist/types/session.js.map +0 -1
  185. package/src/__tests__/clean-state-extra.test.ts +0 -301
  186. package/src/__tests__/clean-state.test.ts +0 -169
  187. package/src/__tests__/constraint-handler.test.ts +0 -132
  188. package/src/__tests__/passes-gate-extra.test.ts +0 -93
  189. package/src/__tests__/session-startup.test.ts +0 -334
  190. package/src/cli/commands/__tests__/analyze-sessions.test.ts +0 -184
  191. package/src/cli/commands/__tests__/update-user-model.test.ts +0 -268
  192. package/src/cli/commands/analyze-sessions.ts +0 -335
  193. package/src/cli/commands/update-user-model.ts +0 -476
  194. package/src/cli/session-mining/__tests__/transcript.test.ts +0 -118
  195. package/src/cli/session-mining/corrections.ts +0 -56
  196. package/src/cli/session-mining/index.ts +0 -13
  197. package/src/cli/session-mining/text.ts +0 -117
  198. package/src/cli/session-mining/transcript.ts +0 -125
  199. package/src/context/__tests__/compaction.test.ts +0 -169
  200. package/src/context/compaction.ts +0 -201
  201. package/src/core/session/__tests__/startup.test.ts +0 -281
  202. package/src/core/session/clean-state.ts +0 -178
  203. package/src/core/session/index.ts +0 -15
  204. package/src/core/session/startup.ts +0 -253
  205. package/src/failure/constraint-handler.ts +0 -171
  206. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +0 -140
  207. package/src/knowledge/lifecycle-hooks.ts +0 -202
  208. package/src/types/session.ts +0 -132
@@ -1,117 +0,0 @@
1
- /**
2
- * 分词 / 停用词 / 相似度 / 文本清洗(工单 19-C)
3
- *
4
- * 合并 analyze-sessions 与 update-user-model 的重复实现。
5
- */
6
-
7
- /** N-gram 停用词表(中英) */
8
- export const STOP_WORDS = new Set([
9
- '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一',
10
- '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着',
11
- '没有', '看', '好', '自己', '这', '他', '她', '它', '们', '那',
12
- 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
13
- 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
14
- 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
15
- 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
16
- 'before', 'after', 'above', 'below', 'between', 'under', 'again',
17
- 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why',
18
- 'how', 'all', 'both', 'each', 'few', 'more', 'most', 'other', 'some',
19
- 'such', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just',
20
- '因为', '所以', '但是', '而且', '如果', '虽然', '可以', '这个', '那个',
21
- '什么', '怎么', '哪', '吗', '吧', '呢', '啊', '嗯', '哦',
22
- ]);
23
-
24
- const CN_CHAR = /[\u4e00-\u9fff\u3400-\u4dbf]/;
25
-
26
- /**
27
- * 中英文混合分词:中文产出 bigram/trigram(短序列保留整体),
28
- * 连续的拉丁/数字/路径字符作为整体 token。
29
- */
30
- export function tokenize(text: string): string[] {
31
- const tokens: string[] = [];
32
- let current = '';
33
-
34
- for (const char of text) {
35
- if (CN_CHAR.test(char)) {
36
- current += char;
37
- } else if (/[a-zA-Z0-9_\-/.]/.test(char)) {
38
- current += char;
39
- } else {
40
- pushToken(current, tokens);
41
- current = '';
42
- }
43
- }
44
- pushToken(current, tokens);
45
-
46
- return tokens;
47
- }
48
-
49
- function pushToken(current: string, tokens: string[]): void {
50
- if (current.length < 2) return;
51
- if (CN_CHAR.test(current)) {
52
- for (let i = 0; i <= current.length - 2; i++) {
53
- tokens.push(current.slice(i, i + 2));
54
- }
55
- for (let i = 0; i <= current.length - 3; i++) {
56
- tokens.push(current.slice(i, i + 3));
57
- }
58
- if (current.length <= 6) tokens.push(current);
59
- } else {
60
- tokens.push(current);
61
- }
62
- }
63
-
64
- /**
65
- * 基于 tokenize 的 Jaccard 相似度
66
- */
67
- export function jaccardSimilarity(a: string, b: string): number {
68
- const setA = new Set(tokenize(a));
69
- const setB = new Set(tokenize(b));
70
- const intersection = new Set([...setA].filter(x => setB.has(x)));
71
- const union = new Set([...setA, ...setB]);
72
- return union.size === 0 ? 0 : intersection.size / union.size;
73
- }
74
-
75
- /**
76
- * 仅取汉字字符集的 Jaccard 相似度(概念聚类用)
77
- */
78
- export function jaccardChinese(a: string, b: string): number {
79
- const charsA = new Set(a.split('').filter(c => /[\u4e00-\u9fff]/.test(c)));
80
- const charsB = new Set(b.split('').filter(c => /[\u4e00-\u9fff]/.test(c)));
81
- if (charsA.size === 0 || charsB.size === 0) return 0;
82
- const intersection = new Set([...charsA].filter(x => charsB.has(x)));
83
- const union = new Set([...charsA, ...charsB]);
84
- return intersection.size / union.size;
85
- }
86
-
87
- /**
88
- * 剥离代码块 / 行内代码 / JSON 片段,保留自然语言
89
- */
90
- export function stripCodeBlocks(text: string): string {
91
- return text
92
- .replace(/```[\s\S]*?```/g, '')
93
- .replace(/`[^`]+`/g, '')
94
- .replace(/\{[^{}]*"[^"]+"\s*:\s*"[^"]*"[^}]*\}/g, '')
95
- .replace(/\[[\s\S]*?\]/g, '')
96
- .replace(/\\n/g, ' ')
97
- .replace(/[{}[\]"':,\\]+/g, ' ');
98
- }
99
-
100
- /** 是否纯标点/空白 */
101
- export function isPunctuation(str: string): boolean {
102
- return /^[,。!?、;:""''()[\]【】「」『』《》〈〉\s]+$/.test(str);
103
- }
104
-
105
- /** 是否含语义内容(汉字,或 ≥7 字符的合法英文标识词) */
106
- export function hasSemanticContent(phrase: string): boolean {
107
- if (/[\u4e00-\u9fff]/.test(phrase)) return true;
108
- if (phrase.length >= 7 && /^[a-zA-Z][a-zA-Z0-9_]*[a-zA-Z]$/.test(phrase)) return true;
109
- return false;
110
- }
111
-
112
- /** 是否代码噪声(短拉丁片段 / 常见 JSON 字段名碎片) */
113
- export function isCodeNoise(phrase: string): boolean {
114
- if (!/[\u4e00-\u9fff]/.test(phrase) && phrase.length <= 6) return true;
115
- if (/^(?:clla|cood|odde|oppe|laaw|peen|enne|ennc|ddec|deco|ecod|code|type|role|name|text|file|path|tool|user|session)$/i.test(phrase)) return true;
116
- return false;
117
- }
@@ -1,125 +0,0 @@
1
- /**
2
- * Claude Code transcript(.jsonl)读取(工单 19-C)
3
- *
4
- * 原 analyze-sessions.findSessions 与 update-user-model.findNewSessions
5
- * 各持一份解析逻辑,收敛于此。
6
- */
7
-
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import { readJsonl } from '../../utils/jsonl';
11
-
12
- export interface MinedTurn {
13
- role: string;
14
- content: string;
15
- }
16
-
17
- export interface MinedSession {
18
- /** 会话 ID(文件名去掉 .jsonl,截断至 40 字符) */
19
- id: string;
20
- /** 文件修改日期(YYYY-MM-DD) */
21
- date: string;
22
- /** 修改时间戳(毫秒) */
23
- mtimeMs: number;
24
- turns: MinedTurn[];
25
- /** 出现过的工具调用名(去重,保持出现顺序) */
26
- toolCalls: string[];
27
- }
28
-
29
- function extractText(content: unknown): string {
30
- if (typeof content === 'string') return content;
31
- if (Array.isArray(content)) {
32
- return content
33
- .filter((b: any) => b.type === 'text')
34
- .map((b: any) => b.text)
35
- .join(' ');
36
- }
37
- return '';
38
- }
39
-
40
- /**
41
- * 扫描目录下的 .jsonl transcript 文件
42
- *
43
- * 不可读/损坏的行静默跳过(与原实现一致)。
44
- */
45
- export interface TranscriptFilter {
46
- /** 只读 mtimeMs >= since 的文件(stat 级过滤,命中即不 parse);缺省不过滤 */
47
- since?: number;
48
- /** 按会话 ID(文件名去 .jsonl、截断 40 字符)排除(文件名级过滤,不 stat 不 parse);缺省不排除 */
49
- excludeIds?: string[];
50
- }
51
-
52
- export function readTranscriptSessions(dir: string, filter: TranscriptFilter = {}): MinedSession[] {
53
- const sessions: MinedSession[] = [];
54
- const exclude = filter.excludeIds && filter.excludeIds.length > 0
55
- ? new Set(filter.excludeIds)
56
- : undefined;
57
-
58
- let files: string[];
59
- try {
60
- files = fs.readdirSync(dir);
61
- } catch {
62
- return sessions;
63
- }
64
-
65
- for (const file of files) {
66
- if (!file.endsWith('.jsonl')) continue;
67
- const id = file.replace('.jsonl', '').slice(0, 40);
68
- if (exclude?.has(id)) continue;
69
- const filePath = path.join(dir, file);
70
-
71
- let stat: fs.Stats;
72
- try {
73
- stat = fs.statSync(filePath);
74
- } catch {
75
- continue;
76
- }
77
- if (filter.since !== undefined && stat.mtimeMs < filter.since) continue;
78
-
79
- const turns: MinedTurn[] = [];
80
- const toolCalls: string[] = [];
81
-
82
- let records: Record<string, any>[];
83
- try {
84
- // 损坏行静默跳过(坏行策略 skip,与原实现一致,harness#82 走 jsonl 正本);
85
- // 外层 catch 兜文件不可读,沿用原「跳过该文件」语义
86
- // 计数去向:豁免(harness#100)——读的是外部 Claude Code transcript(非 harness 写入面)。
87
- // 影响方向如实:坏行使该会话少几条 turn,而 analyze-sessions 的候选按次数阈值筛
88
- // (frequency>=4 / 跨会话>=2 / confidence=len/10)→ 只会少出候选,不会造出候选,且候选须人审;
89
- // 告知需改 MinedSession 形状并波及 analyze-sessions/update-user-model 两个调用方,另票收口
90
- records = readJsonl<Record<string, any>>(filePath, 'skip').records;
91
- } catch {
92
- continue;
93
- }
94
-
95
- for (const entry of records) {
96
- try {
97
- const msg = entry.message;
98
- if (msg?.role && (msg.content || entry.type === 'user')) {
99
- turns.push({ role: msg.role, content: extractText(msg.content) });
100
- }
101
- if (msg?.role === 'assistant' && Array.isArray(msg.content)) {
102
- for (const block of msg.content) {
103
- if (block.type === 'tool_use' && block.name) {
104
- toolCalls.push(block.name);
105
- }
106
- }
107
- }
108
- } catch {
109
- // 单条记录提取异常跳过(原逐行 catch 语义:parse 已由 module skip,此处兜提取)
110
- }
111
- }
112
-
113
- if (turns.length > 0) {
114
- sessions.push({
115
- id,
116
- date: stat.mtime.toISOString().slice(0, 10),
117
- mtimeMs: stat.mtimeMs,
118
- turns,
119
- toolCalls: [...new Set(toolCalls)],
120
- });
121
- }
122
- }
123
-
124
- return sessions;
125
- }
@@ -1,169 +0,0 @@
1
- /**
2
- * SessionCompaction 测试
3
- */
4
-
5
- import { SessionCompaction } from '../compaction';
6
- import type { SessionMessage } from '../types';
7
-
8
- function makeMessages(count: number, contentLength: number = 100): SessionMessage[] {
9
- return Array.from({ length: count }, (_, i) => ({
10
- role: (i % 2 === 0 ? 'user' : 'assistant') as 'user' | 'assistant',
11
- content: `message ${i} `.repeat(contentLength / 10),
12
- timestamp: new Date(Date.now() + i * 1000).toISOString(),
13
- }));
14
- }
15
-
16
- describe('SessionCompaction', () => {
17
- describe('shouldCompact', () => {
18
- it('应该返回 true 当超过触发比例', () => {
19
- const compaction = new SessionCompaction({ triggerRatio: 0.5 });
20
- const messages = makeMessages(10, 500);
21
- // 10 * ~50 tokens each ≈ 500 tokens, budget=800 → ratio > 0.5
22
- expect(compaction.shouldCompact(messages, 800)).toBe(true);
23
- });
24
-
25
- it('应该返回 false 当未超过触发比例', () => {
26
- const compaction = new SessionCompaction({ triggerRatio: 0.9 });
27
- const messages = makeMessages(2, 10);
28
- expect(compaction.shouldCompact(messages, 10000)).toBe(false);
29
- });
30
- });
31
-
32
- describe('compact (eviction)', () => {
33
- it('应该按比例丢弃最早消息', () => {
34
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: false });
35
- const messages = makeMessages(20, 500);
36
- const result = compaction.compact(messages, 1000);
37
- expect(result.level).toBe('eviction');
38
- expect(result.compacted.length).toBeLessThan(messages.length);
39
- expect(result.compacted.length).toBeGreaterThanOrEqual(2);
40
- });
41
-
42
- it('应该保留最近的消息', () => {
43
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: false });
44
- const messages = makeMessages(20, 100);
45
- const result = compaction.compact(messages, 1000);
46
- const lastOriginal = messages[messages.length - 1];
47
- expect(result.compacted[result.compacted.length - 1]).toBe(lastOriginal);
48
- });
49
-
50
- it('应该保留 tool-call 边界对', () => {
51
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: true });
52
- const messages: SessionMessage[] = [
53
- { role: 'user', content: 'a'.repeat(1000), timestamp: 't1' },
54
- { role: 'assistant', content: 'b'.repeat(1000), toolCallId: 'call-1', timestamp: 't2' },
55
- { role: 'tool', content: 'c'.repeat(1000), toolCallId: 'call-1', timestamp: 't3' },
56
- { role: 'user', content: 'd'.repeat(1000), timestamp: 't4' },
57
- { role: 'assistant', content: 'e'.repeat(1000), timestamp: 't5' },
58
- ];
59
- const result = compaction.compact(messages, 500);
60
- // 如果 tool result is kept, its paired assistant call should also be kept
61
- const toolResult = result.compacted.find(m => m.role === 'tool');
62
- if (toolResult) {
63
- const paired = result.compacted.find(m => m.toolCallId === toolResult.toolCallId && m.role === 'assistant');
64
- expect(paired).toBeDefined();
65
- }
66
- });
67
- });
68
-
69
- describe('compact (summary)', () => {
70
- it('应该生成结构化摘要', () => {
71
- const compaction = new SessionCompaction({ level: 'summary' });
72
- const messages = makeMessages(20, 100);
73
- const result = compaction.compact(messages, 1000);
74
- expect(result.level).toBe('summary');
75
- expect(result.summary).toBeDefined();
76
- expect(result.summary).toContain('用户目标');
77
- expect(result.summary).toContain('消息总数');
78
- });
79
-
80
- it('应该保留最近 6 条消息', () => {
81
- const compaction = new SessionCompaction({ level: 'summary' });
82
- const messages = makeMessages(20, 100);
83
- const result = compaction.compact(messages, 1000);
84
- // 1 summary message + 6 recent
85
- expect(result.compacted.length).toBe(7);
86
- });
87
- });
88
-
89
- describe('compact (checkpoint)', () => {
90
- it('应该压缩为单条 checkpoint 消息', () => {
91
- const compaction = new SessionCompaction({ level: 'checkpoint' });
92
- const messages = makeMessages(20, 100);
93
- const result = compaction.compact(messages, 1000);
94
- expect(result.level).toBe('checkpoint');
95
- expect(result.compacted.length).toBe(1);
96
- expect(result.compacted[0].content).toContain('Checkpoint');
97
- });
98
- });
99
-
100
- describe('generateStructuredSummary', () => {
101
- it('应该从用户消息提取目标', () => {
102
- const compaction = new SessionCompaction();
103
- const messages: SessionMessage[] = [
104
- { role: 'user', content: '帮我写一个函数', timestamp: 't1' },
105
- { role: 'assistant', content: '好的', timestamp: 't2' },
106
- ];
107
- const summary = compaction.generateStructuredSummary(messages);
108
- expect(summary).toContain('帮我写一个函数');
109
- });
110
-
111
- it('应该统计工具调用', () => {
112
- const compaction = new SessionCompaction();
113
- const messages: SessionMessage[] = [
114
- { role: 'tool', content: 'result1', timestamp: 't1' },
115
- { role: 'tool', content: 'result2', timestamp: 't2' },
116
- ];
117
- const summary = compaction.generateStructuredSummary(messages);
118
- expect(summary).toContain('2 次');
119
- });
120
- });
121
-
122
- describe('getConfig', () => {
123
- it('应该返回配置', () => {
124
- const compaction = new SessionCompaction({ level: 'summary' });
125
- expect(compaction.getConfig().level).toBe('summary');
126
- });
127
- });
128
-
129
- describe('compact (default level)', () => {
130
- it('应该在未知 level 时回退到 eviction', () => {
131
- const compaction = new SessionCompaction({ level: 'eviction' as any, preserveToolCallPairs: false });
132
- // 直接设置一个不会匹配的 level 来触发 default 分支
133
- (compaction as any).config.level = 'unknown_level';
134
- const messages = makeMessages(20, 500);
135
- const result = compaction.compact(messages, 1000);
136
- expect(result.level).toBe('eviction');
137
- });
138
- });
139
-
140
- describe('preserveToolCallPairs 边界情况', () => {
141
- it('应该在 paired assistant 已在 compacted 中时跳过', () => {
142
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: true });
143
- const messages: SessionMessage[] = [
144
- { role: 'user', content: 'a'.repeat(100), timestamp: 't1' },
145
- { role: 'assistant', content: 'b'.repeat(100), toolCallId: 'call-1', timestamp: 't2' },
146
- { role: 'tool', content: 'c'.repeat(100), toolCallId: 'call-1', timestamp: 't3' },
147
- { role: 'user', content: 'd'.repeat(100), timestamp: 't4' },
148
- { role: 'assistant', content: 'e'.repeat(100), timestamp: 't5' },
149
- ];
150
- // budget 足够大,所有消息都会保留
151
- const result = compaction.compact(messages, 100000);
152
- // paired assistant 已在 compacted 中,不应重复添加
153
- const assistantCalls = result.compacted.filter(m => m.toolCallId === 'call-1' && m.role === 'assistant');
154
- expect(assistantCalls.length).toBe(1);
155
- });
156
-
157
- it('应该处理无 toolCallId 的消息', () => {
158
- const compaction = new SessionCompaction({ level: 'eviction', preserveToolCallPairs: true });
159
- const messages: SessionMessage[] = [
160
- { role: 'user', content: 'a'.repeat(1000), timestamp: 't1' },
161
- { role: 'assistant', content: 'b'.repeat(1000), timestamp: 't2' },
162
- { role: 'user', content: 'c'.repeat(1000), timestamp: 't3' },
163
- { role: 'assistant', content: 'd'.repeat(1000), timestamp: 't4' },
164
- ];
165
- const result = compaction.compact(messages, 500);
166
- expect(result.compacted.length).toBeGreaterThan(0);
167
- });
168
- });
169
- });
@@ -1,201 +0,0 @@
1
- /**
2
- * 会话压缩
3
- *
4
- * 三档策略:eviction / summary / checkpoint
5
- * 不调用 LLM,零 token 成本
6
- */
7
-
8
- import { TokenEstimator } from './token-budget';
9
- import type { CompactionConfig, CompactionLevel, SessionMessage } from './types';
10
- import { DEFAULT_COMPACTION_CONFIG } from './types';
11
-
12
- export interface CompactionResult {
13
- compacted: SessionMessage[];
14
- summary?: string;
15
- level: CompactionLevel;
16
- originalTokens: number;
17
- compactedTokens: number;
18
- }
19
-
20
- export class SessionCompaction {
21
- private config: CompactionConfig;
22
-
23
- constructor(config?: Partial<CompactionConfig>) {
24
- this.config = { ...DEFAULT_COMPACTION_CONFIG, ...config };
25
- }
26
-
27
- /**
28
- * 检查是否需要压缩
29
- */
30
- shouldCompact(messages: SessionMessage[], budget: number): boolean {
31
- const totalTokens = this.estimateMessages(messages);
32
- return totalTokens / budget > this.config.triggerRatio;
33
- }
34
-
35
- /**
36
- * 执行压缩
37
- */
38
- compact(messages: SessionMessage[], budget: number): CompactionResult {
39
- const originalTokens = this.estimateMessages(messages);
40
-
41
- switch (this.config.level) {
42
- case 'eviction':
43
- return this.evictionCompact(messages, budget, originalTokens);
44
- case 'summary':
45
- return this.summaryCompact(messages, budget, originalTokens);
46
- case 'checkpoint':
47
- return this.checkpointCompact(messages, budget, originalTokens);
48
- default:
49
- return this.evictionCompact(messages, budget, originalTokens);
50
- }
51
- }
52
-
53
- /**
54
- * 轻量压缩:按比例丢最早消息,保留 tool-call 边界对
55
- */
56
- private evictionCompact(messages: SessionMessage[], budget: number, originalTokens: number): CompactionResult {
57
- const targetTokens = Math.floor(budget * 0.6); // 目标压缩到 60%
58
- const keepRatio = targetTokens / originalTokens;
59
-
60
- // 至少保留最近 2 条
61
- const minKeep = Math.min(2, messages.length);
62
- const keepCount = Math.max(minKeep, Math.floor(messages.length * keepRatio));
63
-
64
- let compacted = messages.slice(-keepCount);
65
-
66
- // 保留 tool-call 边界对
67
- if (this.config.preserveToolCallPairs) {
68
- compacted = this.preserveToolCallPairs(messages, compacted);
69
- }
70
-
71
- const compactedTokens = this.estimateMessages(compacted);
72
-
73
- return {
74
- compacted,
75
- level: 'eviction',
76
- originalTokens,
77
- compactedTokens,
78
- };
79
- }
80
-
81
- /**
82
- * 中等压缩:结构化总结 + 保留最近 N 轮
83
- */
84
- private summaryCompact(messages: SessionMessage[], _budget: number, originalTokens: number): CompactionResult {
85
- const recentCount = Math.min(6, messages.length); // 保留最近 6 条
86
- const recent = messages.slice(-recentCount);
87
- const older = messages.slice(0, -recentCount);
88
-
89
- // 生成结构化摘要
90
- const summary = this.generateStructuredSummary(older);
91
-
92
- // 摘要作为系统消息插入
93
- const summarySessionMessage: SessionMessage = {
94
- role: 'assistant',
95
- content: `[会话摘要]\n${summary}`,
96
- timestamp: new Date().toISOString(),
97
- };
98
-
99
- const compacted = [summarySessionMessage, ...recent];
100
- const compactedTokens = this.estimateMessages(compacted);
101
-
102
- return {
103
- compacted,
104
- summary,
105
- level: 'summary',
106
- originalTokens,
107
- compactedTokens,
108
- };
109
- }
110
-
111
- /**
112
- * 重型压缩:checkpoint + 清空历史
113
- */
114
- private checkpointCompact(messages: SessionMessage[], _budget: number, originalTokens: number): CompactionResult {
115
- const summary = this.generateStructuredSummary(messages);
116
-
117
- const checkpointSessionMessage: SessionMessage = {
118
- role: 'assistant',
119
- content: `[Checkpoint]\n${summary}\n\n[历史已压缩,共 ${messages.length} 条消息]`,
120
- timestamp: new Date().toISOString(),
121
- };
122
-
123
- const compacted = [checkpointSessionMessage];
124
- const compactedTokens = this.estimateMessages(compacted);
125
-
126
- return {
127
- compacted,
128
- summary,
129
- level: 'checkpoint',
130
- originalTokens,
131
- compactedTokens,
132
- };
133
- }
134
-
135
- /**
136
- * 生成结构化摘要(零 LLM 成本)
137
- */
138
- generateStructuredSummary(messages: SessionMessage[]): string {
139
- const sections: string[] = [];
140
-
141
- // 提取用户目标
142
- const userSessionMessages = messages.filter(m => m.role === 'user');
143
- if (userSessionMessages.length > 0) {
144
- sections.push(`用户目标: ${userSessionMessages[0].content.slice(0, 200)}`);
145
- }
146
-
147
- // 提取工具调用
148
- const toolCalls = messages.filter(m => m.role === 'tool');
149
- if (toolCalls.length > 0) {
150
- sections.push(`工具调用: ${toolCalls.length} 次`);
151
- }
152
-
153
- // 提取关键信息
154
- const assistantSessionMessages = messages.filter(m => m.role === 'assistant');
155
- if (assistantSessionMessages.length > 0) {
156
- const lastAssistant = assistantSessionMessages[assistantSessionMessages.length - 1];
157
- sections.push(`最近响应: ${lastAssistant.content.slice(0, 200)}`);
158
- }
159
-
160
- sections.push(`消息总数: ${messages.length}`);
161
- sections.push(`时间范围: ${messages[0]?.timestamp || 'N/A'} → ${messages[messages.length - 1]?.timestamp || 'N/A'}`);
162
-
163
- return sections.join('\n');
164
- }
165
-
166
- /**
167
- * 保留 tool-call 边界对
168
- *
169
- * 如果 tool 结果被保留,对应的 tool 调用也要保留
170
- */
171
- private preserveToolCallPairs(all: SessionMessage[], compacted: SessionMessage[]): SessionMessage[] {
172
- const result: SessionMessage[] = [];
173
-
174
- for (const msg of compacted) {
175
- // 如果是 tool 结果,确保对应的 tool 调用也在
176
- if (msg.role === 'tool' && msg.toolCallId) {
177
- const paired = all.find(m => m.toolCallId === msg.toolCallId && m.role === 'assistant');
178
- if (paired && !compacted.some(m => m === paired)) {
179
- result.push(paired);
180
- }
181
- }
182
- result.push(msg);
183
- }
184
-
185
- return result;
186
- }
187
-
188
- /**
189
- * 估算消息的 token 数
190
- */
191
- private estimateMessages(messages: SessionMessage[]): number {
192
- return messages.reduce((sum, m) => sum + TokenEstimator.estimateText(m.content), 0);
193
- }
194
-
195
- /**
196
- * 获取配置
197
- */
198
- getConfig(): CompactionConfig {
199
- return { ...this.config };
200
- }
201
- }