dsh-plugin-bridge 0.2.10
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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +146 -0
- package/cordis.patch.yml +18 -0
- package/docs/articles/agent-session-handoff.md +162 -0
- package/docs/benchmark.md +165 -0
- package/docs/design.md +82 -0
- package/docs/guide.zh.md +152 -0
- package/docs/native-webui-feasibility.md +44 -0
- package/docs/plan.md +65 -0
- package/lib/api-rpc.d.ts +64 -0
- package/lib/api-rpc.js +76 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +368 -0
- package/lib/command.d.ts +92 -0
- package/lib/command.js +377 -0
- package/lib/compression.d.ts +90 -0
- package/lib/compression.js +369 -0
- package/lib/fold.d.ts +39 -0
- package/lib/fold.js +406 -0
- package/lib/index.d.ts +55 -0
- package/lib/index.js +76 -0
- package/lib/migrate.d.ts +176 -0
- package/lib/migrate.js +449 -0
- package/lib/rpc.d.ts +34 -0
- package/lib/rpc.js +76 -0
- package/lib/types.d.ts +64 -0
- package/lib/types.js +9 -0
- package/package.json +106 -0
- package/reports/v0.2.3-e2e-2026-08-20T13-19-13-924Z.raw.json +2727 -0
- package/reports/v0.2.3-e2e-report.md +116 -0
- package/reports/v0.2.6-rc11-vision-report.md +65 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/** 摘要正文的硬预算(字符)。默认 2400 字符 ≈ 900 tokens。 */
|
|
2
|
+
export const SUMMARY_CHAR_BUDGET = 2400;
|
|
3
|
+
/** 压缩输入(喂给工人模型的取材)总字符预算,≈30K tokens 内。 */
|
|
4
|
+
export const SOURCE_CHAR_BUDGET = 60_000;
|
|
5
|
+
/** 摘要预算换算成 token 的粗系数(中英混合,~2.67 字符/token)。 */
|
|
6
|
+
const SUMMARY_CHARS_PER_TOKEN = SUMMARY_CHAR_BUDGET / 900;
|
|
7
|
+
/** 单条 assistant 结论段截断。 */
|
|
8
|
+
const ASSISTANT_SNIPPET = 900;
|
|
9
|
+
/** 保留完整细节的最近 assistant 消息条数。 */
|
|
10
|
+
const RECENT_ASSISTANT_MESSAGES = 6;
|
|
11
|
+
/** 逐字视觉证据独立于摘要预算;只按完整块收录,绝不从中间截断。 */
|
|
12
|
+
export const VISUAL_EVIDENCE_CHAR_BUDGET = 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* 各分区的预算配额。
|
|
15
|
+
*
|
|
16
|
+
* 旧实现是「按顺序装,装不下就从当前分区头部切一刀然后 break」,
|
|
17
|
+
* 于是超预算时活下来的是**最老**的用户消息,而承载「刚完成什么 / 卡在哪」的
|
|
18
|
+
* 最近助手结论段会整段消失——正好和迁移需要的相反。现在改成先按配额分配,
|
|
19
|
+
* 未用尽的额度再按 用户消息 > 最近结论 > compaction 底稿 的优先级回流。
|
|
20
|
+
*/
|
|
21
|
+
const SECTION_SHARE = { compaction: 0.25, users: 0.45, recent: 0.3 };
|
|
22
|
+
const SECTION_HEADERS = {
|
|
23
|
+
compaction: '【早前上下文压缩摘要】',
|
|
24
|
+
users: '【用户消息全文(按时间序)】',
|
|
25
|
+
recent: '【最近助手输出摘要】',
|
|
26
|
+
};
|
|
27
|
+
function toolLine(node) {
|
|
28
|
+
// 工具只留名字与首个路径样参数,stdout 与代码块一律丢弃。
|
|
29
|
+
const detail = (node.detail ?? '').split('\n')[0] ?? '';
|
|
30
|
+
const pathMatch = /(?:^|\s)((?:\/|~\/|\.\/|[\w.-]+\/)[\w./-]+)/.exec(detail);
|
|
31
|
+
return pathMatch ? `${node.title} ${pathMatch[1]}` : node.title;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 把“含图用户消息”与它到下一条用户消息之间的助手正文配对。
|
|
35
|
+
*
|
|
36
|
+
* Bridge 不声称这些正文一定是图片描述,只称为“关联助手响应”;这样即使助手
|
|
37
|
+
* 只是追问,也不会被误标成已经识图。文本由程序直接复制,不经过摘要模型。
|
|
38
|
+
*/
|
|
39
|
+
export function collectVisualEvidence(messages, charBudget = VISUAL_EVIDENCE_CHAR_BUDGET) {
|
|
40
|
+
const candidates = [];
|
|
41
|
+
let userMessage = 0;
|
|
42
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
43
|
+
const message = messages[index];
|
|
44
|
+
if (message.role !== 'user')
|
|
45
|
+
continue;
|
|
46
|
+
userMessage += 1;
|
|
47
|
+
const imageCount = message.imageCount ?? 0;
|
|
48
|
+
if (imageCount <= 0)
|
|
49
|
+
continue;
|
|
50
|
+
const assistant = [];
|
|
51
|
+
for (let next = index + 1; next < messages.length; next += 1) {
|
|
52
|
+
const following = messages[next];
|
|
53
|
+
if (following.role === 'user')
|
|
54
|
+
break;
|
|
55
|
+
if (following.role === 'assistant' && following.kind !== 'compaction' && following.content.trim()) {
|
|
56
|
+
assistant.push(following.content.trim());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
candidates.push({
|
|
60
|
+
userMessage,
|
|
61
|
+
imageCount,
|
|
62
|
+
userText: message.content.trim(),
|
|
63
|
+
assistantText: assistant.join('\n\n'),
|
|
64
|
+
attachments: [...(message.imageAttachments ?? [])],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const budget = Math.max(0, charBudget);
|
|
68
|
+
const included = [];
|
|
69
|
+
let used = 0;
|
|
70
|
+
// 最新证据优先,但最终仍按时间顺序呈现;整块装不下就省略,不切正文。
|
|
71
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
72
|
+
const item = candidates[index];
|
|
73
|
+
const cost = item.userText.length + item.assistantText.length + 240;
|
|
74
|
+
if (used + cost > budget)
|
|
75
|
+
continue;
|
|
76
|
+
included.unshift(item);
|
|
77
|
+
used += cost;
|
|
78
|
+
}
|
|
79
|
+
const omitted = candidates.length - included.length;
|
|
80
|
+
const represented = included.filter((item) => item.assistantText.length > 0).length;
|
|
81
|
+
return {
|
|
82
|
+
imageMessages: candidates.length,
|
|
83
|
+
images: candidates.reduce((sum, item) => sum + item.imageCount, 0),
|
|
84
|
+
represented,
|
|
85
|
+
// 未关联正文与预算整块省略都需要人工核验;每条图片消息只计一次。
|
|
86
|
+
unresolved: candidates.length - represented,
|
|
87
|
+
included,
|
|
88
|
+
omitted,
|
|
89
|
+
truncated: omitted > 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** 把逐字视觉证据作为独立附录拼到模型摘要后;正文不会被二次改写。 */
|
|
93
|
+
export function appendVisualEvidence(summary, evidence, lang) {
|
|
94
|
+
if (evidence.imageMessages === 0)
|
|
95
|
+
return summary.trim();
|
|
96
|
+
const blocks = [summary.trim()];
|
|
97
|
+
const represented = evidence.included.filter((item) => item.assistantText);
|
|
98
|
+
if (represented.length) {
|
|
99
|
+
blocks.push(lang === 'en'
|
|
100
|
+
? '## Visual evidence — verbatim, not summarized\nThe associated assistant responses below are copied exactly from the source session. They may be questions or partial analyses; do not claim they prove more than their text says.'
|
|
101
|
+
: '## 视觉证据——原文搬运,未经二次摘要\n以下关联助手响应由程序从源会话逐字复制;它可能是追问或不完整分析,不得声称超出原文的结论。');
|
|
102
|
+
for (const item of represented) {
|
|
103
|
+
const title = lang === 'en'
|
|
104
|
+
? `### Source user message ${item.userMessage} · ${item.imageCount} image(s)`
|
|
105
|
+
: `### 源用户消息 ${item.userMessage} · ${item.imageCount} 张图片`;
|
|
106
|
+
const pieces = [title];
|
|
107
|
+
if (item.userText) {
|
|
108
|
+
pieces.push(lang === 'en' ? '**User context (verbatim)**' : '**用户文字(原文)**', item.userText);
|
|
109
|
+
}
|
|
110
|
+
pieces.push(lang === 'en' ? '**Associated assistant response (verbatim)**' : '**关联助手响应(原文)**', item.assistantText);
|
|
111
|
+
blocks.push(pieces.join('\n\n'));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const unresolved = evidence.included.filter((item) => !item.assistantText);
|
|
115
|
+
if (unresolved.length || evidence.omitted) {
|
|
116
|
+
const lines = [lang === 'en' ? '## Unresolved images' : '## 未解析图片'];
|
|
117
|
+
for (const item of unresolved) {
|
|
118
|
+
lines.push(lang === 'en'
|
|
119
|
+
? `- Source user message ${item.userMessage} contains ${item.imageCount} image(s) with no associated assistant text. Do not infer their contents; reattach or inspect the original session.`
|
|
120
|
+
: `- 源用户消息 ${item.userMessage} 含 ${item.imageCount} 张图片,但没有关联助手正文。不得猜测内容;请重新附图或回源会话检查。`);
|
|
121
|
+
}
|
|
122
|
+
if (evidence.omitted) {
|
|
123
|
+
lines.push(lang === 'en'
|
|
124
|
+
? `- ${evidence.omitted} older visual-evidence block(s) exceeded the dedicated budget and were omitted whole, never partially truncated. Inspect the original session before relying on them.`
|
|
125
|
+
: `- 另有 ${evidence.omitted} 个较早视觉证据块超出独立预算,已整块省略而非截断。依赖这些图片前必须回源会话核验。`);
|
|
126
|
+
}
|
|
127
|
+
blocks.push(lines.join('\n'));
|
|
128
|
+
}
|
|
129
|
+
return blocks.filter(Boolean).join('\n\n');
|
|
130
|
+
}
|
|
131
|
+
/** 在给定字符预算内渲染一个分区;预算连表头都放不下时返回 null。 */
|
|
132
|
+
function renderSection(section, budget) {
|
|
133
|
+
const header = section.header;
|
|
134
|
+
if (budget <= header.length + 1)
|
|
135
|
+
return null;
|
|
136
|
+
const room = budget - header.length - 1; // -1: 表头与正文之间的换行
|
|
137
|
+
if (section.keep === 'head') {
|
|
138
|
+
const body = section.items.join('\n');
|
|
139
|
+
if (body.length <= room)
|
|
140
|
+
return { text: `${header}\n${body}`, clipped: false };
|
|
141
|
+
const marker = '\n…(底稿截断)';
|
|
142
|
+
// 只放得下截断标记本身就没有信息量了,整段让位给优先级更高的分区。
|
|
143
|
+
if (room <= marker.length)
|
|
144
|
+
return null;
|
|
145
|
+
const slice = body.slice(0, room - marker.length);
|
|
146
|
+
return { text: `${header}\n${slice}${marker}`, clipped: true };
|
|
147
|
+
}
|
|
148
|
+
// keep === 'newest':从最后一条往前收,直到装不下。
|
|
149
|
+
const picked = [];
|
|
150
|
+
let used = 0;
|
|
151
|
+
let index = section.items.length - 1;
|
|
152
|
+
for (; index >= 0; index -= 1) {
|
|
153
|
+
const item = section.items[index];
|
|
154
|
+
const cost = item.length + (picked.length ? 1 : 0);
|
|
155
|
+
if (used + cost > room)
|
|
156
|
+
break;
|
|
157
|
+
picked.unshift(item);
|
|
158
|
+
used += cost;
|
|
159
|
+
}
|
|
160
|
+
const droppedCount = index + 1;
|
|
161
|
+
if (!picked.length)
|
|
162
|
+
return null;
|
|
163
|
+
if (droppedCount > 0) {
|
|
164
|
+
const note = section.note?.(droppedCount) ?? `(较早的 ${droppedCount} 条因预算省略)`;
|
|
165
|
+
// 提示行本身也要占额度:装不下就再让出一条。
|
|
166
|
+
while (picked.length > 1 && used + note.length + 1 > room) {
|
|
167
|
+
const shed = picked.shift();
|
|
168
|
+
used -= shed.length + 1;
|
|
169
|
+
}
|
|
170
|
+
return { text: `${header}\n${note}\n${picked.join('\n')}`, clipped: true };
|
|
171
|
+
}
|
|
172
|
+
return { text: `${header}\n${picked.join('\n')}`, clipped: false };
|
|
173
|
+
}
|
|
174
|
+
/** 从折叠消息构建压缩输入。messages 按时间正序。 */
|
|
175
|
+
export function buildBridgeSource(messages, options = {}) {
|
|
176
|
+
const budget = Math.max(0, options.sourceCharBudget ?? SOURCE_CHAR_BUDGET);
|
|
177
|
+
const visualEvidence = collectVisualEvidence(messages, options.visualEvidenceCharBudget);
|
|
178
|
+
// 1) 最近一次 compaction 摘要当底稿(官方已付过压缩成本)。
|
|
179
|
+
const compaction = [...messages].reverse().find((m) => m.kind === 'compaction' && m.content.trim());
|
|
180
|
+
// 2) 用户消息全文(意图锚点,体量小)。
|
|
181
|
+
const users = messages.filter((m) => m.role === 'user' && (m.content.trim() || (m.imageCount ?? 0) > 0));
|
|
182
|
+
// 3) 最近若干条 assistant 结论 + 工具痕迹(只留名字与路径)。
|
|
183
|
+
const assistants = messages.filter((m) => m.role === 'assistant' && (m.content.trim() || (m.toolNodes?.length ?? 0) > 0));
|
|
184
|
+
const recent = assistants.slice(-RECENT_ASSISTANT_MESSAGES);
|
|
185
|
+
const sections = [];
|
|
186
|
+
if (compaction) {
|
|
187
|
+
sections.push({
|
|
188
|
+
name: 'compaction',
|
|
189
|
+
header: SECTION_HEADERS.compaction,
|
|
190
|
+
items: [compaction.content.trim()],
|
|
191
|
+
keep: 'head',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (users.length) {
|
|
195
|
+
sections.push({
|
|
196
|
+
name: 'users',
|
|
197
|
+
header: SECTION_HEADERS.users,
|
|
198
|
+
items: users.map((m, i) => {
|
|
199
|
+
const marker = (m.imageCount ?? 0) > 0
|
|
200
|
+
? `[image attachments: ${m.imageCount}; visual content is not available to the summary worker]`
|
|
201
|
+
: '';
|
|
202
|
+
return `${i + 1}. ${[marker, m.content.trim()].filter(Boolean).join(' ')}`;
|
|
203
|
+
}),
|
|
204
|
+
keep: 'newest',
|
|
205
|
+
note: (dropped) => `(较早的 ${dropped} 条用户消息因预算省略,保留的是最近的)`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (recent.length) {
|
|
209
|
+
const blocks = recent.map((m) => {
|
|
210
|
+
const parts = [];
|
|
211
|
+
const text = m.content.trim();
|
|
212
|
+
if (text)
|
|
213
|
+
parts.push(text.length > ASSISTANT_SNIPPET ? `${text.slice(0, ASSISTANT_SNIPPET)}…` : text);
|
|
214
|
+
const tools = (m.toolNodes ?? []).map(toolLine);
|
|
215
|
+
if (tools.length)
|
|
216
|
+
parts.push(`[工具] ${tools.join(';')}`);
|
|
217
|
+
return parts.join('\n');
|
|
218
|
+
});
|
|
219
|
+
sections.push({
|
|
220
|
+
name: 'recent',
|
|
221
|
+
header: SECTION_HEADERS.recent,
|
|
222
|
+
items: blocks.map((block, i) => (i === blocks.length - 1 ? block : `${block}\n---`)),
|
|
223
|
+
keep: 'newest',
|
|
224
|
+
note: (dropped) => `(更早的 ${dropped} 条助手输出因预算省略)`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (!sections.length) {
|
|
228
|
+
return {
|
|
229
|
+
text: '',
|
|
230
|
+
userMessagesUsed: 0,
|
|
231
|
+
userMessagesTotal: users.length,
|
|
232
|
+
reusedCompaction: false,
|
|
233
|
+
truncated: false,
|
|
234
|
+
dropped: [],
|
|
235
|
+
visualEvidence,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// 分区间用空行拼接,先把分隔符的开销从总预算里扣掉。
|
|
239
|
+
const separators = (sections.length - 1) * 2;
|
|
240
|
+
const usable = Math.max(0, budget - separators);
|
|
241
|
+
// 4) 先按配额分配,未用尽的额度再按优先级回流。
|
|
242
|
+
const need = new Map();
|
|
243
|
+
for (const section of sections) {
|
|
244
|
+
const body = section.keep === 'head'
|
|
245
|
+
? section.items.join('\n').length
|
|
246
|
+
: section.items.reduce((sum, item) => sum + item.length + 1, -1);
|
|
247
|
+
need.set(section.name, section.header.length + 1 + Math.max(0, body));
|
|
248
|
+
}
|
|
249
|
+
const alloc = new Map();
|
|
250
|
+
let pool = usable;
|
|
251
|
+
for (const section of sections) {
|
|
252
|
+
const quota = Math.floor(usable * SECTION_SHARE[section.name]);
|
|
253
|
+
const take = Math.min(need.get(section.name) ?? 0, quota);
|
|
254
|
+
alloc.set(section.name, take);
|
|
255
|
+
pool -= take;
|
|
256
|
+
}
|
|
257
|
+
for (const name of ['users', 'recent', 'compaction']) {
|
|
258
|
+
if (pool <= 0)
|
|
259
|
+
break;
|
|
260
|
+
const section = sections.find((s) => s.name === name);
|
|
261
|
+
if (!section)
|
|
262
|
+
continue;
|
|
263
|
+
const gap = (need.get(name) ?? 0) - (alloc.get(name) ?? 0);
|
|
264
|
+
if (gap <= 0)
|
|
265
|
+
continue;
|
|
266
|
+
const extra = Math.min(gap, pool);
|
|
267
|
+
alloc.set(name, (alloc.get(name) ?? 0) + extra);
|
|
268
|
+
pool -= extra;
|
|
269
|
+
}
|
|
270
|
+
// 5) 渲染。
|
|
271
|
+
const picked = [];
|
|
272
|
+
const dropped = [];
|
|
273
|
+
let usersUsed = 0;
|
|
274
|
+
for (const section of sections) {
|
|
275
|
+
const rendered = renderSection(section, alloc.get(section.name) ?? 0);
|
|
276
|
+
if (!rendered) {
|
|
277
|
+
dropped.push(section.name);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (rendered.clipped)
|
|
281
|
+
dropped.push(section.name);
|
|
282
|
+
if (section.name === 'users') {
|
|
283
|
+
// 数一下真正进了正文的条目(提示行不算)。
|
|
284
|
+
usersUsed = rendered.text.split('\n').filter((line) => /^\d+\. /.test(line)).length;
|
|
285
|
+
}
|
|
286
|
+
picked.push(rendered.text);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
text: picked.join('\n\n'),
|
|
290
|
+
userMessagesUsed: usersUsed,
|
|
291
|
+
userMessagesTotal: users.length,
|
|
292
|
+
reusedCompaction: Boolean(compaction) && !dropped.includes('compaction'),
|
|
293
|
+
truncated: dropped.length > 0 || visualEvidence.truncated,
|
|
294
|
+
dropped: visualEvidence.truncated ? [...dropped, 'visual-evidence'] : dropped,
|
|
295
|
+
visualEvidence,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** 摘要预算(字符)换算成写进指令的 token 上限。 */
|
|
299
|
+
export function summaryTokenBudget(summaryCharBudget = SUMMARY_CHAR_BUDGET) {
|
|
300
|
+
return Math.max(100, Math.round(summaryCharBudget / SUMMARY_CHARS_PER_TOKEN));
|
|
301
|
+
}
|
|
302
|
+
/** 压缩指令:让工人模型输出固定 schema 的交接摘要。 */
|
|
303
|
+
export function buildBridgeInstruction(lang, options = {}) {
|
|
304
|
+
const tokens = summaryTokenBudget(options.summaryCharBudget);
|
|
305
|
+
if (lang === 'en') {
|
|
306
|
+
return `You are a handoff engineer. Below is material from an AI coding session (full user messages, recent assistant output, and possibly an earlier compaction summary).
|
|
307
|
+
|
|
308
|
+
Write a handoff summary for ANOTHER agent taking over this task under a DIFFERENT tool preset. Follow this structure exactly, ≤${tokens} tokens total:
|
|
309
|
+
|
|
310
|
+
## Goal
|
|
311
|
+
(1-2 sentences: what the user is building and the definition of done)
|
|
312
|
+
## Current state
|
|
313
|
+
(3-5 sentences: progress, what was just completed, any blocker)
|
|
314
|
+
## Key decisions & conventions
|
|
315
|
+
(≤5 bullets: technical choices, user preferences, hard constraints — include the reasoning)
|
|
316
|
+
## Key files
|
|
317
|
+
(≤10 paths, one per line, optional half-sentence note)
|
|
318
|
+
## Next step
|
|
319
|
+
(1-2 sentences)
|
|
320
|
+
|
|
321
|
+
Rules: drop details tied to the old preset's tools; keep decision rationale; when the material says a value, path, dependency, or convention was superseded/revoked, OMIT that obsolete concrete value entirely and keep only the currently effective replacement (never list an obsolete value even to say it is obsolete); every path must come from the material, never invent one; copy exact current numbers (ports, versions, limits) verbatim on their own line so they cannot be rounded to common values; no pleasantries — output the summary only.
|
|
322
|
+
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
return `你是一名交接工程师。下面是某个 AI 编程会话的取材(用户消息全文、最近若干轮助手输出,可能还有早前的一次上下文压缩摘要)。
|
|
326
|
+
|
|
327
|
+
请为「即将在另一套工具模式下接管任务的 agent」写一份交接摘要,严格按以下结构,总长 ≤${tokens} tokens:
|
|
328
|
+
|
|
329
|
+
## 目标
|
|
330
|
+
(1-2 句:用户在做什么、完成的定义)
|
|
331
|
+
## 当前状态
|
|
332
|
+
(3-5 句:进展到哪、刚完成什么、卡在哪)
|
|
333
|
+
## 关键决策与约定
|
|
334
|
+
(≤5 条:技术选型、用户偏好、硬约束——附决策理由)
|
|
335
|
+
## 关键文件
|
|
336
|
+
(≤10 个路径,一行一个,可带半句说明)
|
|
337
|
+
## 下一步
|
|
338
|
+
(1-2 句)
|
|
339
|
+
|
|
340
|
+
要求:删去与原模式工具细节相关的内容;保留决策理由;取材若明确说明某个值、路径、依赖或约定已作废/被覆盖,必须彻底省略该旧具体值,只保留当前生效的替代值(即使为了说明“已作废”也不得复述旧值);路径必须来自取材,不得编造;当前生效的端口、版本号、数量上限这类精确数字必须原样单独成行抄写,不得改写成常见值;不要寒暄,直接输出摘要。
|
|
341
|
+
|
|
342
|
+
`;
|
|
343
|
+
}
|
|
344
|
+
/** 注入新会话首轮的交接指令(goal 之后的第一条 prompt)。 */
|
|
345
|
+
export function buildBridgeKickoff(lang, autoContinue = false) {
|
|
346
|
+
if (lang === 'en') {
|
|
347
|
+
return autoContinue
|
|
348
|
+
? 'The session goal above is a handoff summary from a previous session that ran under a different tool preset. Treat only currently effective values as actionable; never quote or restate concrete values marked obsolete, revoked, or superseded. Reply in one short paragraph restating your understanding of the current state, then continue with the next step.'
|
|
349
|
+
: 'The session goal above is a handoff summary from a previous session that ran under a different tool preset. Treat only currently effective values as actionable; never quote or restate concrete values marked obsolete, revoked, or superseded. Reply in one short paragraph restating your understanding of the current state, then stop and wait for the user to confirm before taking any further action.';
|
|
350
|
+
}
|
|
351
|
+
return autoContinue
|
|
352
|
+
? '上面的会话目标是上个会话(另一套工具模式)留下的交接摘要。只把当前生效值当作可执行事实;不要引用或复述任何标记为已作废、撤销或被覆盖的具体旧值。请先用一段话复述你对当前状态的理解,然后继续执行下一步。'
|
|
353
|
+
: '上面的会话目标是上个会话(另一套工具模式)留下的交接摘要。只把当前生效值当作可执行事实;不要引用或复述任何标记为已作废、撤销或被覆盖的具体旧值。请只用一段话复述你对当前状态的理解,然后停止,等待用户确认后再采取任何进一步行动。';
|
|
354
|
+
}
|
|
355
|
+
/** 成本预估(粗):按取材字符数估输入 tokens,中英混合按 ~2 字符/token。 */
|
|
356
|
+
export function estimateSummaryTokens(sourceChars, options = {}) {
|
|
357
|
+
return { input: Math.ceil(sourceChars / 2) + 400, output: summaryTokenBudget(options.summaryCharBudget) };
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* 取材语言判定:CJK 字符占比超过 15% 视为中文。
|
|
361
|
+
* 用于 `--lang auto`:摘要语言应该跟着会话内容走,而不是跟着部署默认走。
|
|
362
|
+
*/
|
|
363
|
+
export function detectLang(text) {
|
|
364
|
+
const sample = text.slice(0, 4000);
|
|
365
|
+
if (!sample)
|
|
366
|
+
return 'en';
|
|
367
|
+
const cjk = sample.match(/[一-鿿-ヿ]/g)?.length ?? 0;
|
|
368
|
+
return cjk / sample.length > 0.15 ? 'zh' : 'en';
|
|
369
|
+
}
|
package/lib/fold.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把 `session.history` 的原始事件折叠成会话消息。
|
|
3
|
+
*
|
|
4
|
+
* 迁移链路上的第一步:取材、摘要、注入全都建立在折叠结果上,折叠漏一种事件
|
|
5
|
+
* 就等于摘要少一段事实,而且不会报错。所以这份折叠器是产品代码(`src/`)而不是
|
|
6
|
+
* 评测脚本的附属物,并有独立测试覆盖。
|
|
7
|
+
*
|
|
8
|
+
* 只保留迁移需要的部分:用户消息、助手结论、工具痕迹、compaction 检查点。
|
|
9
|
+
* 实时渲染相关的增量合并(mergeLive / liveStep / ledger)属于 GUI,不在此处。
|
|
10
|
+
*/
|
|
11
|
+
import type { ChatMessage, SessionEvent } from './types.ts';
|
|
12
|
+
/** 工具调用的入参摘要:命令 / 路径 / 查询串,取第一个有值的。 */
|
|
13
|
+
export declare function toolDetail(data: Record<string, unknown> | undefined): string;
|
|
14
|
+
/** 工具输出(折叠保留,取材不使用)。 */
|
|
15
|
+
export declare function toolOutput(data: Record<string, unknown> | undefined): string;
|
|
16
|
+
/**
|
|
17
|
+
* 是否是 compaction 检查点消息。
|
|
18
|
+
*
|
|
19
|
+
* 上游把这个判据专门导出成 `isCompactCheckpointSource`
|
|
20
|
+
* (`@deepseek-ai/dsh-compaction/checkpoint`,一个不依赖 cordis 的纯谓词出口,
|
|
21
|
+
* 就是给客户端/wire 程序用的)。这里保持同一语义:认 provenance 标记,
|
|
22
|
+
* 文本标签只作为兜底。
|
|
23
|
+
*/
|
|
24
|
+
export declare function isCompactCheckpoint(data: Record<string, unknown> | undefined, text?: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* 取出检查点里真正的摘要正文。
|
|
27
|
+
*
|
|
28
|
+
* 上游 `frameSummary()` 拼出的形状是
|
|
29
|
+
* `CHECKPOINT_PREAMBLE + "\n\n<compacted-summary>" + 摘要 + "</compacted-summary>"`,
|
|
30
|
+
* 而 preamble 是一句面向模型的指令("把它当既有背景,别提这个 checkpoint,
|
|
31
|
+
* 直接继续")。只去标签会把这句指令一起喂给压缩工人,所以这里连 preamble 一起剥掉;
|
|
32
|
+
* 找不到标签时退回原文。
|
|
33
|
+
*/
|
|
34
|
+
export declare function stripCompactTags(text: string): string;
|
|
35
|
+
/** `turn:step`,两者都在 payload 上时才有值。 */
|
|
36
|
+
export declare function stepKey(event: SessionEvent): string | null;
|
|
37
|
+
export declare function toolName(data: Record<string, unknown> | undefined): string;
|
|
38
|
+
/** 把一页 history(或实时 mux 事件)折叠成会话消息,按时间正序。 */
|
|
39
|
+
export declare function foldSessionEvents(events: SessionEvent[]): ChatMessage[];
|