Skip to content

Commit 1a0a9d2

Browse files
committed
调整记忆和压缩生成内容
1 parent b365735 commit 1a0a9d2

10 files changed

Lines changed: 78 additions & 140 deletions

File tree

‎docs/context.md‎

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,52 +29,46 @@ Coding Code 采用两层压缩策略,在不同阈值下自动触发:
2929
|--------|-----|------|
3030
| 触发阈值 | `promptEstimate > modelMaxTokens * 0.9` | prompt 估算超过模型最大 token 90% 时触发 |
3131
| 保留最近 turn | 1 | 保留最近 1 个 turn 不压缩 |
32-
| 压缩方式 | 调用 LLM 生成摘要 | 输出 `<summary>...</summary>` 块 |
32+
| 压缩方式 | 调用 LLM 生成摘要 | 整段输出即摘要(全量替换,不做标签抽取) |
3333
| 增量压缩 | 是 | 找到已有 SummaryEvent,只压缩 `endTurnId` 之后的事件 |
3434
| 失败追踪 | 连续 3 次失败后停止 | 24 小时 TTL 后重置 |
3535

3636
---
3737

3838
## 压缩输出格式
3939

40-
LLM 压缩的摘要包含 10 个固定小节:
40+
LLM 压缩要求模型按 10 个固定小节输出;**模型的整段输出即摘要文本**,不做标签抽取:
4141

4242
```
43-
<analysis>
44-
自由推理区域,分析对话内容和关键信息
45-
</analysis>
46-
47-
<summary>
48-
### Primary Request
43+
## 1. Primary Request and Intent
4944
用户的核心请求
5045
51-
### Key Technical Concepts
46+
## 2. Key Technical Concepts
5247
涉及的关键技术概念
5348
54-
### Files and Code Sections
49+
## 3. Files and Code Sections
5550
相关文件和代码段
5651
57-
### Errors and Fixes
52+
## 4. Errors and Fixes
5853
遇到的错误和修复
5954
60-
### Problem Solving
55+
## 5. Problem Solving
6156
问题解决过程
6257
63-
### Decision Rationale
64-
决策理由
58+
## 6. Decision Rationale and Rejected Approaches
59+
决策理由与被否决的方案
6560
66-
### All User Messages
67-
所有用户消息摘要
61+
## 7. All User Messages
62+
所有用户消息
6863
69-
### Pending Tasks
64+
## 8. Pending Tasks
7065
待处理任务
7166
72-
### Current Work
67+
## 9. Current Work
7368
当前工作内容
7469
75-
### Optional Next Step
70+
## 10. Optional Next Step
7671
可选的下一步
77-
</summary>
7872
```
7973

8074
---

‎docs/memory.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ Coding Code 支持跨会话的长期记忆:自动从对话中提取关键信
3131

3232
1. 读取记忆文件全文作为"已有记忆"
3333
2. 将会话记录(按 `[user]` / `[assistant]` / `[tool:名称]` 标注)与已有记忆一起发送给 LLM
34-
3. LLM 输出整份**最新版记忆**,放在 `<memory>...</memory>` 块中
34+
3. LLM 输出整份**最新版记忆**(整段输出即内容,不做标签或片段抽取)
3535
4. 直接用输出内容整体替换记忆文件(受字节上限约束)
3636

37-
模型自行决定更新哪些内容:可以新增条目、修改过时信息、删除不再相关的内容,代码不做"模型只改动哪部分"的任何假设。若模型没有输出有效内容、或输出与当前文件一致,则不写入。
37+
模型自行决定更新哪些内容:可以新增条目、修改过时信息、删除不再相关的内容,代码不做"模型只改动哪部分"的任何假设。若模型输出为空、或输出与当前文件一致,则不写入。
3838

3939
### 提取提示词
4040

‎packages/codingcode/src/context/compaction-prompt.ts‎

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
export const COMPACTION_SYSTEM_PROMPT = `You analyze and then summarize an agent conversation transcript.
1+
export const COMPACTION_SYSTEM_PROMPT = `Summarize an agent conversation transcript into the sections below.
22
3-
Output exactly two top-level blocks:
4-
5-
<analysis>
6-
Free-form notes about the conversation. Identify the user's goal, what was done, what was learned, what remains. This block is for your reasoning — be thorough.
7-
</analysis>
8-
9-
<summary>
103
## 1. Primary Request and Intent
114
The user's overall objective and concrete asks.
125
@@ -35,5 +28,4 @@ Work the user explicitly asked for that is not yet done.
3528
What was happening at the moment of compaction.
3629
3730
## 10. Optional Next Step
38-
A recommended next action consistent with the user's intent.
39-
</summary>`;
31+
A recommended next action consistent with the user's intent.`;

‎packages/codingcode/src/context/context.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -380,12 +380,11 @@ export const ContextLayer = Layer.effect(ContextService, Effect.gen(function* ()
380380
.complete({ messages: [userMsg], system }, model)
381381
.pipe(Effect.either);
382382
if (result._tag === 'Left') return null;
383-
return extractSummary(result.right.content.trim());
383+
return normalizeSummary(result.right.content);
384384
}).pipe(Effect.catchAllCause(() => Effect.succeed(null)));
385385

386-
function extractSummary(raw: string): string {
387-
const m = raw.match(/<summary>([\s\S]*?)<\/summary>/);
388-
return (m?.[1] ?? raw).trim();
386+
function normalizeSummary(raw: string): string {
387+
return raw.trim();
389388
}
390389

391390
const willCompact = (

‎packages/codingcode/src/memory/extractor.ts‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
import { Effect } from 'effect';
22
import type { LLMShape } from '../llm/port.js';
33

4-
const SYSTEM_PROMPT = `你是记忆整理器。基于"已有记忆"和"会话记录",输出整份最新版长期记忆,放在 <memory>...</memory> 块中,不要输出其它内容。
4+
const SYSTEM_PROMPT = `你是记忆整理器。基于"已有记忆"和"会话记录",输出整份最新版长期记忆。
55
66
规则:
77
- 只保留值得跨会话记住的信息:用户角色、偏好与对 Agent 的纠正,项目架构决策、技术选型与部署信息,外部资源与链接等。
88
- 忽略临时内容:一次性任务、调试过程、报错堆栈、闲聊。
99
- 更新哪些内容由你决定:在已有记忆基础上自行增、删、改,输出必须是一份完整、自洽的最新记忆,而不是只输出变动部分。
1010
- 旧记忆与对话新信息矛盾时以最新为准;同一会话前后不一致时以最后出现为准。
1111
- 不要编造对话中未出现的信息。
12-
- 若没有值得记住的新信息且已有记忆为空,输出 <memory></memory>。
1312
1413
格式:
1514
- 纯 Markdown,用 "### 主题" 小节组织,小节下用 "- " 列要点。
16-
- 条目需具体、自包含,避免"上面提到的那个"这类指代。
17-
- <memory> 内不要带任何解释性文字。`;
15+
- 条目需具体、自包含,避免"上面提到的那个"这类指代。`;
1816

19-
function extractFrom(content: string): string | null {
20-
const memoryMatch = content.match(/<memory>([\s\S]*?)<\/memory>/);
21-
if (!memoryMatch) return null;
22-
return memoryMatch[1]!.trim() || null;
17+
function normalizeMemoryOutput(content: string): string | null {
18+
return content.trim() || null;
2319
}
2420

2521
export function extractMemory(opts: {
@@ -45,7 +41,7 @@ ${transcript || '(空)'}`;
4541
model
4642
)
4743
.pipe(
48-
Effect.map((res) => extractFrom(res.content)),
44+
Effect.map((res) => normalizeMemoryOutput(res.content)),
4945
Effect.catchAllCause(() => Effect.succeed(null))
5046
);
5147
}

‎packages/codingcode/test/context/budget-integration.test.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { randomUUID } from 'crypto';
55
import { Effect, Layer } from 'effect';
66
import { ContextService } from '../../src/context/port.js';
77
import type { ContextShape } from '../../src/context/port.js';
8-
import { SessionService } from '../../src/session/port.js';
98
import { SessionLayer } from '../../src/session/session.js';
109
import { LLMService } from '../../src/llm/port.js';
1110
import type { SessionRef } from '../../src/contracts/session.js';

‎packages/codingcode/test/context/compressor/behavior.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ describe('compressor behavior', () => {
134134
const summaries = readSummaryEvents(fx.transcriptPath);
135135
expect(summaries.length).toBe(1);
136136
expect(summaries[0]!.summaryText).toContain('### Goal');
137+
// 全量替换:模型原文即摘要,不做任何抽取或裁剪
138+
expect(summaries[0]!.summaryText).toBe(summary);
137139
expect(summaries[0]!.startTurnId).toBeLessThanOrEqual(summaries[0]!.endTurnId);
138140
expect(summaries[0]!.endTurnId).toBeGreaterThan(0);
139141
} finally {
Lines changed: 28 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,35 @@
11
import { describe, it, expect } from 'vitest';
2-
3-
describe('L5 compaction prompt and extraction', () => {
4-
it('should extract summary from dual-tag format', () => {
5-
const raw = `<analysis>
6-
This is analysis section with reasoning.
7-
Multiple lines of thinking.
8-
</analysis>
9-
10-
<summary>
11-
## 1. Primary Request and Intent
12-
User wants to add feature X.
13-
14-
## 2. Key Technical Concepts
15-
- Concept A
16-
- Concept B
17-
18-
## 9. Optional Next Step
19-
Consider optimizing Y next.
20-
</summary>
21-
22-
Some trailing text that should be ignored.`;
23-
24-
const match = raw.match(/<summary>([\s\S]*?)<\/summary>/);
25-
expect(match).toBeDefined();
26-
expect(match?.[1]).toContain('Primary Request');
27-
expect(match?.[1]).toContain('Optional Next Step');
2+
import { COMPACTION_SYSTEM_PROMPT } from '../../../src/context/compaction-prompt.js';
3+
4+
// 断言直接读真实提示词常量,避免测试与实现各写一份而悄悄漂移。
5+
describe('L5 compaction prompt contract', () => {
6+
const SECTIONS = [
7+
'## 1. Primary Request and Intent',
8+
'## 2. Key Technical Concepts',
9+
'## 3. Files and Code Sections',
10+
'## 4. Errors and Fixes',
11+
'## 5. Problem Solving',
12+
'## 6. Decision Rationale and Rejected Approaches',
13+
'## 7. All User Messages',
14+
'## 8. Pending Tasks',
15+
'## 9. Current Work',
16+
'## 10. Optional Next Step',
17+
];
18+
19+
it('requests all ten sections in order', () => {
20+
let cursor = -1;
21+
for (const section of SECTIONS) {
22+
const at = COMPACTION_SYSTEM_PROMPT.indexOf(section);
23+
expect(at, `missing or out-of-order section: ${section}`).toBeGreaterThan(cursor);
24+
cursor = at;
25+
}
2826
});
2927

30-
it('should fallback to raw content if no summary tags', () => {
31-
const raw = 'Just raw content without tags';
32-
const match = raw.match(/<summary>([\s\S]*?)<\/summary>/);
33-
const result = match ? match[1] : raw;
34-
expect(result).toBe(raw);
28+
it('no longer asks for a separate analysis block', () => {
29+
expect(COMPACTION_SYSTEM_PROMPT).not.toMatch(/<\/?analysis>/);
3530
});
3631

37-
it('should validate 9 sections are present in summary structure', () => {
38-
const sections = [
39-
'## 1. Primary Request and Intent',
40-
'## 2. Key Technical Concepts',
41-
'## 3. Files and Code Sections',
42-
'## 4. Errors and Fixes',
43-
'## 5. Problem Solving',
44-
'## 6. All User Messages',
45-
'## 7. Pending Tasks',
46-
'## 8. Current Work',
47-
'## 9. Optional Next Step',
48-
];
49-
50-
sections.forEach((section) => {
51-
expect(section).toMatch(/^##\s+\d+\./);
52-
});
53-
expect(sections).toHaveLength(9);
54-
});
55-
56-
it('should separate analysis from summary', () => {
57-
const full = `<analysis>
58-
Reasoning and thinking process here.
59-
This is how I approached the problem.
60-
</analysis>
61-
62-
<summary>
63-
The final structured output.
64-
</summary>`;
65-
66-
const analysisMatch = full.match(/<analysis>([\s\S]*?)<\/analysis>/);
67-
const summaryMatch = full.match(/<summary>([\s\S]*?)<\/summary>/);
68-
69-
expect(analysisMatch?.[1]).toContain('Reasoning');
70-
expect(summaryMatch?.[1]).toContain('final structured');
71-
expect(analysisMatch?.[1]).not.toContain('final structured');
72-
});
73-
74-
it('should handle empty sections gracefully', () => {
75-
const raw = `<analysis></analysis>
76-
<summary>
77-
## 1. Primary Request and Intent
78-
</summary>`;
79-
80-
const match = raw.match(/<summary>([\s\S]*?)<\/summary>/);
81-
expect(match?.[1]).toBeDefined();
32+
it('no longer wraps the summary in tags', () => {
33+
expect(COMPACTION_SYSTEM_PROMPT).not.toMatch(/<\/?summary>/);
8234
});
8335
});

‎packages/codingcode/test/memory/extractor.test.ts‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,28 @@ function extract(llm: LLMShape, currentMemory: string, transcript: string) {
2020
}
2121

2222
describe('Memory Extractor', () => {
23-
it('returns memory inside <memory> tags', async () => {
24-
const response = `<memory>### 主题
25-
- 用户是 TypeScript 开发者</memory>`;
23+
it('takes the whole model output as the new memory', async () => {
24+
const response = `### 主题
25+
- 用户是 TypeScript 开发者`;
2626

2727
const result = await extract(createMockLlm(response), '', '[user] I like TypeScript');
2828

29-
expect(result).toContain('### 主题');
30-
expect(result).toContain('用户是 TypeScript 开发者');
29+
expect(result).toBe(response);
3130
});
3231

33-
it('returns null when memory tags are empty', async () => {
34-
const result = await extract(createMockLlm('<memory></memory>'), '', '[user] Some text');
32+
it('keeps a preamble instead of trying to strip it', async () => {
33+
const response = `好的,我整理了一份记忆:
3534
36-
expect(result).toBeNull();
35+
### 主题
36+
- 用户偏好简洁回答`;
37+
38+
const result = await extract(createMockLlm(response), '', '[user] 简洁点');
39+
40+
expect(result).toBe(response);
3741
});
3842

39-
it('returns null when memory tags not found', async () => {
40-
const result = await extract(createMockLlm('No memory tags here'), '', '[user] Some text');
43+
it('returns null when the model returns blank output', async () => {
44+
const result = await extract(createMockLlm(' \n '), '', '[user] Some text');
4145

4246
expect(result).toBeNull();
4347
});
@@ -56,7 +60,7 @@ describe('Memory Extractor', () => {
5660
});
5761

5862
it('passes currentMemory to the model as existing memory', async () => {
59-
const mockLlm = createMockLlm('<memory></memory>');
63+
const mockLlm = createMockLlm('');
6064

6165
await extract(mockLlm, '### project\n- 旧信息', '[user] 新对话');
6266

@@ -67,7 +71,7 @@ describe('Memory Extractor', () => {
6771
});
6872

6973
it('keeps instructions in system and transcript data in messages', async () => {
70-
const mockLlm = createMockLlm('<memory></memory>');
74+
const mockLlm = createMockLlm('');
7175

7276
await extract(mockLlm, '### project\n- Likes TypeScript', '[user] I use Python');
7377

@@ -80,7 +84,7 @@ describe('Memory Extractor', () => {
8084
});
8185

8286
it('passes the target model to the non-streaming channel', async () => {
83-
const mockLlm = createMockLlm('<memory></memory>');
87+
const mockLlm = createMockLlm('');
8488

8589
await extract(mockLlm, '', '[user] hi');
8690

‎packages/codingcode/test/memory/index.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ describe('flushSessionToMemory', () => {
173173
{ type: 'user', content: '记住新架构决策' },
174174
{ type: 'assistant', content: '好的' },
175175
] as any);
176-
setLlmResponse('<memory>### 项目\n- 新的架构决策</memory>');
176+
setLlmResponse('### 项目\n- 新的架构决策');
177177

178178
const result = await run(service.flushSessionToMemory('session', TEST_MODEL, tmpDir));
179179

@@ -183,14 +183,14 @@ describe('flushSessionToMemory', () => {
183183
expect(fs.readFileSync(memFile, 'utf-8')).toBe('### 项目\n- 新的架构决策');
184184
});
185185

186-
it('keeps file unchanged when model returns empty memory', async () => {
186+
it('keeps file unchanged when the model returns blank output', async () => {
187187
await enableConfig();
188188
writeMemory('### 旧主题\n- 旧内容');
189189
const { readTranscript } = await import('../../src/session/file-ops.js');
190190
vi.mocked(readTranscript).mockImplementation(() => [
191191
{ type: 'user', content: 'hello' },
192192
] as any);
193-
setLlmResponse('<memory></memory>');
193+
setLlmResponse('');
194194

195195
const result = await run(service.flushSessionToMemory('session', TEST_MODEL, tmpDir));
196196

@@ -205,7 +205,7 @@ describe('flushSessionToMemory', () => {
205205
vi.mocked(readTranscript).mockImplementation(() => [
206206
{ type: 'user', content: '无新信息' },
207207
] as any);
208-
setLlmResponse('<memory>### 主题\n- 不变的内容</memory>');
208+
setLlmResponse('### 主题\n- 不变的内容');
209209

210210
const result = await run(service.flushSessionToMemory('session', TEST_MODEL, tmpDir));
211211

@@ -219,7 +219,7 @@ describe('flushSessionToMemory', () => {
219219
vi.mocked(readTranscript).mockImplementation(() => [
220220
{ type: 'user', content: 'hello' },
221221
] as any);
222-
setLlmResponse('<memory>### 自动\n- 新记忆</memory>', () => {
222+
setLlmResponse('### 自动\n- 新记忆', () => {
223223
writeMemory('### 手动\n- 用户并发编辑');
224224
});
225225

0 commit comments

Comments
 (0)