mocode-ai 1.2.7 → 1.2.9
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/dist/agent/core.js +126 -43
- package/dist/config/index.js +31 -6
- package/dist/config/presets.js +36 -1
- package/dist/context/budget.js +5 -16
- package/dist/llm/index.js +42 -4
- package/dist/repl/index.js +57 -13
- package/dist/session/compact.js +329 -40
- package/dist/session/notes.js +63 -0
- package/dist/session/scheduler.js +5 -9
- package/dist/tools/constants.js +21 -0
- package/package.json +1 -1
- package/dist/__trace_manual_test__.js +0 -1
- package/dist/agent/core.d.ts +0 -112
- package/dist/agent/index.d.ts +0 -19
- package/dist/agent/mode.d.ts +0 -29
- package/dist/agent/retry-classifier.js +0 -64
- package/dist/agent/spawn.d.ts +0 -36
- package/dist/attachments/image.d.ts +0 -22
- package/dist/commands/config.d.ts +0 -8
- package/dist/commands/skill.js +0 -230
- package/dist/config/file.d.ts +0 -23
- package/dist/config/index.d.ts +0 -114
- package/dist/config/presets.d.ts +0 -57
- package/dist/context/budget.d.ts +0 -79
- package/dist/context/classifier.d.ts +0 -14
- package/dist/context/encoders/_util.d.ts +0 -11
- package/dist/context/encoders/code.d.ts +0 -2
- package/dist/context/encoders/doc.d.ts +0 -14
- package/dist/context/encoders/graph.d.ts +0 -11
- package/dist/context/encoders/graph.js +0 -81
- package/dist/context/encoders/index.d.ts +0 -2
- package/dist/context/encoders/log.d.ts +0 -2
- package/dist/context/encoders/log.js +0 -2
- package/dist/context/encoders/memory.d.ts +0 -2
- package/dist/context/encoders/passthrough.d.ts +0 -11
- package/dist/context/encoders/search.d.ts +0 -2
- package/dist/context/encoders/summary.d.ts +0 -12
- package/dist/context/encoders/table.d.ts +0 -2
- package/dist/context/encoders/tree.d.ts +0 -2
- package/dist/context/index.d.ts +0 -6
- package/dist/context/lifecycle.d.ts +0 -51
- package/dist/context/pipeline.d.ts +0 -9
- package/dist/context/registry.d.ts +0 -9
- package/dist/context/relevance.d.ts +0 -65
- package/dist/context/types.d.ts +0 -36
- package/dist/foo.js +0 -28
- package/dist/foo.test.js +0 -12
- package/dist/index.d.ts +0 -1
- package/dist/llm/capabilities.d.ts +0 -19
- package/dist/llm/index.d.ts +0 -111
- package/dist/memory/discover.d.ts +0 -15
- package/dist/memory/index.d.ts +0 -13
- package/dist/memory/reflect.d.ts +0 -34
- package/dist/memory/store.d.ts +0 -124
- package/dist/pet/bridge.d.ts +0 -73
- package/dist/pet/protocol.d.ts +0 -83
- package/dist/pet/state.d.ts +0 -28
- package/dist/plan/active.d.ts +0 -23
- package/dist/plan/active.js +0 -45
- package/dist/plan/index.d.ts +0 -13
- package/dist/plan/index.js +0 -33
- package/dist/plan/state.d.ts +0 -2
- package/dist/plan/state.js +0 -11
- package/dist/plan/store.d.ts +0 -78
- package/dist/plan/store.js +0 -429
- package/dist/project-skill/index.js +0 -95
- package/dist/project-skill/initializer.js +0 -178
- package/dist/project-snapshot/index.js +0 -85
- package/dist/project-snapshot/llm-snapshot.js +0 -150
- package/dist/project-snapshot/static-files.js +0 -87
- package/dist/repl/index.d.ts +0 -22
- package/dist/rollback/index.d.ts +0 -79
- package/dist/sandbox/command.d.ts +0 -3
- package/dist/sandbox/index.d.ts +0 -4
- package/dist/sandbox/jail.d.ts +0 -17
- package/dist/sandbox/policy.d.ts +0 -21
- package/dist/sandbox/root.d.ts +0 -5
- package/dist/session/compact.d.ts +0 -92
- package/dist/session/index.d.ts +0 -13
- package/dist/session/notes-plan.js +0 -41
- package/dist/session/persist.d.ts +0 -30
- package/dist/session/scheduler.d.ts +0 -53
- package/dist/skills/discover.d.ts +0 -33
- package/dist/skills/index.d.ts +0 -13
- package/dist/skills/skill-eval.js +0 -345
- package/dist/skills/skill-improve.js +0 -221
- package/dist/skills/stats.js +0 -102
- package/dist/tools/builtins/apply-patch.js +0 -174
- package/dist/tools/builtins/ask-human.d.ts +0 -9
- package/dist/tools/builtins/codegraph.d.ts +0 -2
- package/dist/tools/builtins/codegraph.js +0 -128
- package/dist/tools/builtins/edit-file.d.ts +0 -2
- package/dist/tools/builtins/glob.d.ts +0 -2
- package/dist/tools/builtins/grep.d.ts +0 -2
- package/dist/tools/builtins/index.d.ts +0 -2
- package/dist/tools/builtins/memory-forget.d.ts +0 -2
- package/dist/tools/builtins/memory-list.d.ts +0 -2
- package/dist/tools/builtins/memory-save.d.ts +0 -2
- package/dist/tools/builtins/memory-search.d.ts +0 -2
- package/dist/tools/builtins/memory-update.d.ts +0 -2
- package/dist/tools/builtins/project-skill-update.js +0 -102
- package/dist/tools/builtins/read-file.d.ts +0 -2
- package/dist/tools/builtins/run-command.d.ts +0 -2
- package/dist/tools/builtins/switch-mode.d.ts +0 -2
- package/dist/tools/builtins/switch-mode.js +0 -44
- package/dist/tools/builtins/task.d.ts +0 -2
- package/dist/tools/builtins/todolist.d.ts +0 -2
- package/dist/tools/builtins/todolist.js +0 -411
- package/dist/tools/builtins/use-skill.d.ts +0 -2
- package/dist/tools/builtins/web-fetch.d.ts +0 -2
- package/dist/tools/builtins/web-search.d.ts +0 -2
- package/dist/tools/builtins/write-file.d.ts +0 -2
- package/dist/tools/constants.d.ts +0 -52
- package/dist/tools/registry.d.ts +0 -18
- package/dist/tools/retry.js +0 -137
- package/dist/tools/types.d.ts +0 -42
- package/dist/ui/batch.d.ts +0 -67
- package/dist/ui/clipboard.d.ts +0 -9
- package/dist/ui/content.d.ts +0 -55
- package/dist/ui/diff.d.ts +0 -34
- package/dist/ui/intervention.d.ts +0 -46
- package/dist/ui/layout.d.ts +0 -225
- package/dist/ui/markdown.d.ts +0 -46
- package/dist/ui/mouse.d.ts +0 -29
- package/dist/ui/prompt.d.ts +0 -61
- package/dist/ui/render.d.ts +0 -60
- package/dist/ui/spinner.d.ts +0 -26
- package/dist/ui/theme.d.ts +0 -47
- package/dist/ui/thinking.js +0 -80
package/dist/session/compact.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chat, chatTools, correctTokenEstimate, estimatePromptTokens, estimateTokens, } from '../llm/index.js';
|
|
2
2
|
import { config } from '../config/index.js';
|
|
3
|
-
import { MAX_HISTORY_RESULT, MAX_MEMORY_RESULT, MAX_OLD_TOOL_STUB, MAX_SKILL_RESULT } from '../tools/constants.js';
|
|
3
|
+
import { MAX_HISTORY_RESULT, MAX_MEMORY_RESULT, MAX_OLD_TOOL_STUB, MAX_SKILL_RESULT, SUMMARY_KEYFACTS_MAX_CHARS, SUMMARY_KEYFACTS_MIN_CHARS, SUMMARY_KEYFACTS_WINDOW_RATIO, SUMMARY_MSG_MAX_CHARS, SUMMARY_OUTPUT_MAX_CHARS, SUMMARY_TRANSCRIPT_WINDOW_RATIO, } from '../tools/constants.js';
|
|
4
4
|
import { ui } from '../ui/theme.js';
|
|
5
5
|
import { Spinner } from '../ui/spinner.js';
|
|
6
6
|
import * as layout from '../ui/layout.js';
|
|
@@ -8,6 +8,7 @@ import { pruneAfterCompaction } from '../rollback/index.js';
|
|
|
8
8
|
import { toText } from '../context/utils.js';
|
|
9
9
|
import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
|
|
10
10
|
import { collectArtifactRefs } from '../context/artifacts.js';
|
|
11
|
+
import { writeCompactionSnapshot } from './notes.js';
|
|
11
12
|
export function createContextState() {
|
|
12
13
|
return { lastEstimate: 0, correction: 1, calibrationSamples: 0 };
|
|
13
14
|
}
|
|
@@ -130,6 +131,155 @@ export function stripImagesForSummary(m) {
|
|
|
130
131
|
}
|
|
131
132
|
return m;
|
|
132
133
|
}
|
|
134
|
+
// ── 摘要钉住:Key Facts 跨代累积,叙述段照常滚动压缩 ──────────────────────
|
|
135
|
+
//
|
|
136
|
+
// 为什么需要:摘要写在 history[1](role:'system'),而 groupFromEnd 只排除 history[0],
|
|
137
|
+
// 于是它被当成普通 group 排到最老位置 → 下次压缩进 oldGroups → 被喂给摘要器再摘要一遍。
|
|
138
|
+
// 每代压缩都对上一代摘要做一次有损再压缩,首轮的用户约束经 2-3 代即糊掉。堵住这个衰减
|
|
139
|
+
// 源比设计任何打分模型都划算。
|
|
140
|
+
//
|
|
141
|
+
// 为什么只钉 Key Facts:钉整段(含 Objective/Completed/Next Steps)会线性膨胀——那些
|
|
142
|
+
// 叙述重跑一遍命令、重读一次文件就能拿回来;Key Facts 才是不在就真没了的东西(路径、
|
|
143
|
+
// 约束、失败结论),且每代只增加几十字符。
|
|
144
|
+
/** 摘要消息头。budget.ts 用同一前缀识别摘要层,勿改。 */
|
|
145
|
+
export const SUMMARY_MARKER = '# 会话摘要';
|
|
146
|
+
/** `## Key Facts` 段标题(model 按 prompt 输出,可能带 "— 说明" 后缀)。 */
|
|
147
|
+
const KEY_FACTS_HEADING_RE = /^ {0,3}##\s*key facts\b/i;
|
|
148
|
+
/** 任意 `## ` 级标题:Key Facts 段到此为止。 */
|
|
149
|
+
const ANY_HEADING_RE = /^ {0,3}##\s+\S/;
|
|
150
|
+
/** 上一代叙述段进 transcript 时的标注:告诉摘要器这是已压过一遍的内容,合并而非抄录。 */
|
|
151
|
+
const PRIOR_NARRATIVE_LABEL = '[prior summary — 上一代摘要的叙述部分:与下方新内容合并压缩,不要重复罗列细节]';
|
|
152
|
+
/** 尾部 provenance 块:解析时剥掉,重建时按当次 older 重算(否则跨代累积并重复)。 */
|
|
153
|
+
const ARTIFACT_REFS_RE = /\n*\[artifact refs:[^\]]*\]\s*$/;
|
|
154
|
+
/** Key Facts 累计预算(字符):窗口比例换算 + 上下限——小窗口留得住初始约束,
|
|
155
|
+
* 大窗口也不会让索引段膨胀成正文。 */
|
|
156
|
+
export function keyFactsBudgetChars(window) {
|
|
157
|
+
const byWindow = Math.floor(Math.max(0, window) * SUMMARY_KEYFACTS_WINDOW_RATIO);
|
|
158
|
+
return Math.max(SUMMARY_KEYFACTS_MIN_CHARS, Math.min(SUMMARY_KEYFACTS_MAX_CHARS, byWindow));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 拆摘要成「Key Facts 段」+「叙述段」。
|
|
162
|
+
* 顺带剥掉 `# 会话摘要` 头与尾部 `[artifact refs: …]` provenance——后者每次压缩按当次
|
|
163
|
+
* older 重算,不该跨代累积。无 Key Facts 段时 rest 为全文、keyFacts 为空。
|
|
164
|
+
*/
|
|
165
|
+
export function splitSummaryText(raw) {
|
|
166
|
+
let text = (raw ?? '').replace(ARTIFACT_REFS_RE, '').trim();
|
|
167
|
+
if (text.startsWith(SUMMARY_MARKER)) {
|
|
168
|
+
const nl = text.indexOf('\n');
|
|
169
|
+
text = (nl < 0 ? '' : text.slice(nl + 1)).trim();
|
|
170
|
+
}
|
|
171
|
+
const lines = text.split('\n');
|
|
172
|
+
const headIdx = lines.findIndex((l) => KEY_FACTS_HEADING_RE.test(l));
|
|
173
|
+
if (headIdx < 0)
|
|
174
|
+
return { keyFacts: '', rest: text };
|
|
175
|
+
let endIdx = lines.length;
|
|
176
|
+
for (let i = headIdx + 1; i < lines.length; i++) {
|
|
177
|
+
if (ANY_HEADING_RE.test(lines[i])) {
|
|
178
|
+
endIdx = i;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const keyFacts = lines.slice(headIdx + 1, endIdx).join('\n').trim();
|
|
183
|
+
const rest = [...lines.slice(0, headIdx), ...lines.slice(endIdx)].join('\n').trim();
|
|
184
|
+
return { keyFacts, rest };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 中断判定:signal 已 abort,或错误本身是 AbortError / APIUserAbortError。
|
|
188
|
+
* 与 llm 层、core 层的判定保持同一套(两处 name + signal 双判),避免各写各的漏判。
|
|
189
|
+
*/
|
|
190
|
+
function isAbortError(err, signal) {
|
|
191
|
+
if (signal?.aborted)
|
|
192
|
+
return true;
|
|
193
|
+
// 鸭子类型判定而非 instanceof Error:DOMException / SDK 的 APIUserAbortError 在
|
|
194
|
+
// 不同 Node 版本下 instanceof Error 的结果不一致,认 name 更稳。
|
|
195
|
+
if (!err || typeof err !== 'object')
|
|
196
|
+
return false;
|
|
197
|
+
const name = err.name;
|
|
198
|
+
return name === 'AbortError' || name === 'APIUserAbortError';
|
|
199
|
+
}
|
|
200
|
+
/** 归一化一行事实用于去重:去项目符号/空白/句末标点后小写。 */
|
|
201
|
+
function factKey(line) {
|
|
202
|
+
return line
|
|
203
|
+
.toLowerCase()
|
|
204
|
+
.replace(/\s+/g, ' ')
|
|
205
|
+
.replace(/^[-*•]\s*/, '')
|
|
206
|
+
.replace(/[。.,;;::]+$/, '')
|
|
207
|
+
.trim();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 从摘要正文提取「当前进度快照」段:Objective / In Progress / Next Steps。
|
|
211
|
+
* 这是压缩后最该被记住的「做到哪了」,固结到 notes.md 供恢复步骤注入。
|
|
212
|
+
* 只识别 `## ` 级标题(摘要器固定输出此结构);一个都没有时返 null(不写空快照)。
|
|
213
|
+
*/
|
|
214
|
+
export function extractProgressSnapshot(summaryRest) {
|
|
215
|
+
const WANT = new Set(['objective', 'in progress', 'next steps']);
|
|
216
|
+
const lines = summaryRest.split('\n');
|
|
217
|
+
const out = [];
|
|
218
|
+
let taking = false;
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const m = line.match(/^ {0,3}##\s+(.+?)\s*$/);
|
|
221
|
+
if (m) {
|
|
222
|
+
const name = m[1].replace(/\s*[—–-].*$/, '').trim().toLowerCase();
|
|
223
|
+
taking = WANT.has(name);
|
|
224
|
+
}
|
|
225
|
+
if (taking)
|
|
226
|
+
out.push(line);
|
|
227
|
+
}
|
|
228
|
+
const text = out.join('\n').trim();
|
|
229
|
+
return text ? text : null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 合并跨代 Key Facts:按行去重(老 → 新),超预算时丢较新条目。
|
|
233
|
+
*
|
|
234
|
+
* 丢新不丢旧的理由:最老那几条通常是首轮用户约束(不可重建,丢了就永远没了);
|
|
235
|
+
* 较新的事实通常在本代叙述段或保留区里还有副本。
|
|
236
|
+
*/
|
|
237
|
+
export function mergeKeyFacts(prev, next, maxChars) {
|
|
238
|
+
const lines = [];
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
for (const block of [prev ?? '', next ?? '']) {
|
|
241
|
+
for (const rawLine of block.split('\n')) {
|
|
242
|
+
const line = rawLine.trim();
|
|
243
|
+
if (!line)
|
|
244
|
+
continue;
|
|
245
|
+
const key = factKey(line);
|
|
246
|
+
if (!key || seen.has(key))
|
|
247
|
+
continue;
|
|
248
|
+
seen.add(key);
|
|
249
|
+
lines.push(line);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const cap = Math.max(0, Math.floor(maxChars));
|
|
253
|
+
const text = lines.join('\n');
|
|
254
|
+
if (text.length <= cap)
|
|
255
|
+
return text;
|
|
256
|
+
const kept = [];
|
|
257
|
+
let len = 0;
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
if (len + line.length + 1 > cap)
|
|
260
|
+
break;
|
|
261
|
+
kept.push(line);
|
|
262
|
+
len += line.length + 1;
|
|
263
|
+
}
|
|
264
|
+
return `${kept.join('\n')}\n…[Key Facts 超预算,已省略较新条目]`;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 非空 user group 判定。LLM API(OpenAI / Anthropic)要求 messages 至少含一条**非空**
|
|
268
|
+
* user 消息,llm/chatOnce 入口有 hasNonEmptyUser 守卫,缺则直接抛。压缩重建 history 后
|
|
269
|
+
* 必须仍有这样一条,否则下一步 chat() 当场失败——见下方 user 保留兜底。
|
|
270
|
+
*/
|
|
271
|
+
function isUserGroup(g) {
|
|
272
|
+
const a = g.assistant;
|
|
273
|
+
return !!a && a.role === 'user' && toText(a.content).trim().length > 0;
|
|
274
|
+
}
|
|
275
|
+
/** 摘要 group 判定:非 history[0] 的 system 消息,以 `# 会话摘要` 开头,且不带 tool_calls。 */
|
|
276
|
+
function isSummaryGroup(g) {
|
|
277
|
+
const a = g.assistant;
|
|
278
|
+
return (!!a &&
|
|
279
|
+
a.role === 'system' &&
|
|
280
|
+
g.tools.length === 0 &&
|
|
281
|
+
toText(a.content).startsWith(SUMMARY_MARKER));
|
|
282
|
+
}
|
|
133
283
|
/** 从尾向头划分 group;history[0](system)排除。连续 tool 归到前导 assistant。 */
|
|
134
284
|
function groupFromEnd(history) {
|
|
135
285
|
const groups = [];
|
|
@@ -221,45 +371,112 @@ function microcompactGroup(g) {
|
|
|
221
371
|
return done;
|
|
222
372
|
}
|
|
223
373
|
// ── 默认摘要器:复用 chat(),空 handlers 不打印 ──────────────────────────
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
374
|
+
/** 单条消息封顶:中间段省略、保头 + 尾(头有路径/意图,尾有结论/报错),总长 ≤ max。 */
|
|
375
|
+
function capMessageText(text, max) {
|
|
376
|
+
if (text.length <= max)
|
|
377
|
+
return text;
|
|
378
|
+
const removed = text.length - max;
|
|
379
|
+
const marker = `\n…[中间 ${removed} 字符已省略]…\n`;
|
|
380
|
+
let remain = max - marker.length;
|
|
381
|
+
if (remain <= 0)
|
|
382
|
+
return text.slice(0, Math.max(0, max)) + marker;
|
|
383
|
+
const head = Math.ceil(remain * 0.6);
|
|
384
|
+
return text.slice(0, head) + marker + text.slice(text.length - (remain - head));
|
|
385
|
+
}
|
|
386
|
+
/** tool_call 参数封顶:只保头(路径/目标等关键信息在开头,长正文对摘要无信息量)。 */
|
|
387
|
+
function capArgsHead(args, max) {
|
|
388
|
+
if (args.length <= max)
|
|
389
|
+
return args;
|
|
390
|
+
return args.slice(0, max) + ` …[另有 ${args.length - max} 字符已省略]`;
|
|
391
|
+
}
|
|
392
|
+
/** 摘要输出硬上限:按最后换行处裁断(不留半句),防模型不听话产出巨长摘要撑大 history。 */
|
|
393
|
+
function capSummaryOutput(summary) {
|
|
394
|
+
if (summary.length <= SUMMARY_OUTPUT_MAX_CHARS)
|
|
395
|
+
return summary;
|
|
396
|
+
const cut = summary.lastIndexOf('\n', SUMMARY_OUTPUT_MAX_CHARS);
|
|
397
|
+
const at = cut > SUMMARY_OUTPUT_MAX_CHARS * 0.5 ? cut : SUMMARY_OUTPUT_MAX_CHARS;
|
|
398
|
+
return summary.slice(0, at) + '\n[摘要输出超长,已截断]';
|
|
399
|
+
}
|
|
400
|
+
/** 按封顶配额拼转录。scale ∈ (0,1] 等比缩小各角色配额(总预算不足时用)。 */
|
|
401
|
+
function buildTranscript(stripped, scale) {
|
|
402
|
+
const caps = SUMMARY_MSG_MAX_CHARS;
|
|
403
|
+
const capFor = (role) => Math.max(200, Math.floor(scale *
|
|
404
|
+
(role === 'user'
|
|
405
|
+
? caps.user
|
|
406
|
+
: role === 'assistant'
|
|
407
|
+
? caps.assistant
|
|
408
|
+
: role === 'tool'
|
|
409
|
+
? caps.tool
|
|
410
|
+
: caps.other)));
|
|
411
|
+
return stripped
|
|
228
412
|
.map((m) => {
|
|
229
|
-
const
|
|
230
|
-
let line = `${role}: ${toText(m.content)}`;
|
|
413
|
+
const cap = capFor(m.role);
|
|
414
|
+
let line = `${m.role}: ${capMessageText(toText(m.content), cap)}`;
|
|
231
415
|
const tcs = m.tool_calls;
|
|
232
416
|
if (tcs) {
|
|
233
417
|
for (const tc of tcs) {
|
|
234
|
-
|
|
418
|
+
const args = tc?.function?.arguments ?? '';
|
|
419
|
+
line += `\n [tool_call ${tc?.function?.name}] ${capArgsHead(args, Math.max(200, Math.floor(scale * caps.tool)))}`;
|
|
235
420
|
}
|
|
236
421
|
}
|
|
237
422
|
return line;
|
|
238
423
|
})
|
|
239
424
|
.join('\n');
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
425
|
+
}
|
|
426
|
+
async function defaultSummarize(older, focus, signal) {
|
|
427
|
+
// 已中断就别白拼转录了(几千 token 的字符串拼接 + 一去不回的 LLM 请求)。
|
|
428
|
+
if (signal?.aborted) {
|
|
429
|
+
throw new DOMException('This operation was aborted', 'AbortError');
|
|
430
|
+
}
|
|
431
|
+
// 摘要前剥离多模态 user 消息里的图片(base64 会撑爆摘要 prompt;image 对摘要无信息量)。
|
|
432
|
+
const stripped = older.map(stripImagesForSummary);
|
|
433
|
+
// 触发压缩时旧区约占窗口 50-60%,摘要器共享同一窗口——逐条封顶后旧区原文即可装入,
|
|
434
|
+
// 不需要"先压一遍再摘要"。封顶策略:先按满额封顶;总量仍超窗口 55% 预算时等比缩小
|
|
435
|
+
// 配额重拼(优先保条数/每轮都有代表,其次保单条长度);再超才整段中截兜底(罕见)。
|
|
436
|
+
let transcript = buildTranscript(stripped, 1);
|
|
437
|
+
const tokenBudget = Math.floor(config.contextWindowTokens * SUMMARY_TRANSCRIPT_WINDOW_RATIO);
|
|
438
|
+
let tokens = estimateTokens(transcript);
|
|
439
|
+
if (tokens > tokenBudget) {
|
|
440
|
+
transcript = buildTranscript(stripped, tokenBudget / tokens);
|
|
441
|
+
tokens = estimateTokens(transcript);
|
|
442
|
+
if (tokens > Math.floor(config.contextWindowTokens * 0.6)) {
|
|
443
|
+
transcript = truncateMid(transcript, Math.floor(config.contextWindowTokens * 0.5));
|
|
444
|
+
}
|
|
244
445
|
}
|
|
245
446
|
const sysMsg = {
|
|
246
447
|
role: 'system',
|
|
247
|
-
content: 'You are
|
|
448
|
+
content: 'You are an aggressive session compressor writing a handoff note for an agent that lost its context. ' +
|
|
449
|
+
'The agent will continue the task with ONLY your summary plus a few most-recent messages, so your summary is the sole memory of everything older.\n' +
|
|
450
|
+
'Output ONLY the summary body in this exact structure (omit empty sections):\n' +
|
|
451
|
+
'## Objective — the user\'s core request(s); cover EVERY distinct user request in the transcript, in order, noting which are completed vs pending.\n' +
|
|
452
|
+
'## Completed — what is already done: files created/modified (exact paths), key change per file, commands run and their outcomes (pass/fail, key numbers), decisions made and why.\n' +
|
|
453
|
+
'## In Progress — what is being worked on right now and exactly where it stopped (e.g. "edit applied to foo.ts, test not yet run").\n' +
|
|
454
|
+
'## Next Steps — the concrete next actions in order.\n' +
|
|
455
|
+
'## Key Facts — only what later steps cannot work without: exact paths, symbols/API shapes, artifact IDs/hashes, constraints, open questions, failed approaches and errors to avoid repeating.\n' +
|
|
456
|
+
'What counts as important (keep): user requests and constraints; final state of each modified file; conclusions and results, not the steps that led there; decisions with reasons; precise references (paths, symbols, hashes, commands) that later steps must cite; failures and what was tried, so mistakes are not repeated.\n' +
|
|
457
|
+
'What to drop: verbatim file contents and tool-output dumps, step-by-step recaps, exploration dead-ends that led nowhere, polite chatter, anything re-derivable by re-reading files.\n' +
|
|
458
|
+
'Rules: total ≤ 400 words. State conclusions and locations (path:line where useful), never paste content. ' +
|
|
459
|
+
'The Key Facts section is carried forward verbatim into future summaries: write each fact as one self-contained line ' +
|
|
460
|
+
'(no pronouns, no "as above", no cross-references), so it still reads correctly after later compactions. ' +
|
|
461
|
+
'Never invent facts, paths, or results that are not in the transcript; if unsure whether something happened, omit it.',
|
|
248
462
|
};
|
|
249
463
|
const userMsg = {
|
|
250
464
|
role: 'user',
|
|
251
465
|
content: focus
|
|
252
|
-
?
|
|
253
|
-
:
|
|
466
|
+
? `请将以下会话历史压缩成交接摘要,重点保留与「${focus}」相关的事实/决策/文件改动:\n\n${transcript}\n\n摘要:`
|
|
467
|
+
: `请将以下会话历史压缩成交接摘要:\n\n${transcript}\n\n摘要:`,
|
|
254
468
|
};
|
|
255
469
|
const spinner = new Spinner((msg, frame) => layout.setStatus(msg, frame ?? undefined));
|
|
256
|
-
spinner.start('压缩中');
|
|
470
|
+
spinner.start(signal ? '压缩中(Ctrl+C 取消)' : '压缩中');
|
|
257
471
|
try {
|
|
258
|
-
|
|
472
|
+
// signal 必须透传:摘要是几十秒的 LLM 调用,不串进来 Ctrl+C 只能干等它跑完。
|
|
473
|
+
// 空 handlers:不打印、不外显流式;tools=[] 不带工具表——摘要纯文本任务,
|
|
474
|
+
// 全量工具 schema 白占几千 token 窗口,还诱导幻觉工具调用。
|
|
475
|
+
const r = await chat([sysMsg, userMsg], {}, signal, []);
|
|
259
476
|
// 推理模型可能只返 reasoning_content(content 为 null),或幻觉出 tool_calls → 视为失败
|
|
260
477
|
if (r.toolCalls.length > 0 || !r.content)
|
|
261
478
|
return null;
|
|
262
|
-
return r.content;
|
|
479
|
+
return capSummaryOutput(r.content);
|
|
263
480
|
}
|
|
264
481
|
finally {
|
|
265
482
|
spinner.stop();
|
|
@@ -275,9 +492,28 @@ export async function compactHistory(history, opts) {
|
|
|
275
492
|
const activeTools = opts.tools ?? chatTools;
|
|
276
493
|
const estimateBefore = estimatePromptTokens(history, activeTools, state.correction);
|
|
277
494
|
state.lastEstimate = estimateBefore;
|
|
278
|
-
|
|
495
|
+
// 调用前就已中断(用户在上一步末尾按的 Ctrl+C):一步都别做,直接冒泡。
|
|
496
|
+
// 不做完再抛是为了保证 history 完全未被触碰——abortRestore 才还原得干净。
|
|
497
|
+
if (opts.signal?.aborted) {
|
|
498
|
+
throw new DOMException('This operation was aborted', 'AbortError');
|
|
499
|
+
}
|
|
500
|
+
const allGroups = groupFromEnd(history);
|
|
501
|
+
// 摘要钉住:把已有摘要从 group 序列里摘出来,永不送进摘要器(否则每代再摘要一次 →
|
|
502
|
+
// 递归衰减)。它的 Key Facts 段跨代累积,叙述段则并入下方转录照常滚动压缩。
|
|
503
|
+
const pinnedGroups = allGroups.filter(isSummaryGroup);
|
|
504
|
+
const groups = pinnedGroups.length > 0 ? allGroups.filter((g) => !isSummaryGroup(g)) : allGroups;
|
|
505
|
+
const pinned = pinnedGroups.length > 0
|
|
506
|
+
? splitSummaryText(pinnedGroups
|
|
507
|
+
.map((g) => toText(g.assistant?.content))
|
|
508
|
+
.join('\n\n'))
|
|
509
|
+
: null;
|
|
279
510
|
// 保近期:按策略中的 token 比例累积(至少保 1 组),永不劈开 group。
|
|
280
|
-
|
|
511
|
+
// force(手动 /compact 默认、硬闸触发)用更激进的保留比例;再加绝对上限,
|
|
512
|
+
// 防大窗口(如 256k)下保留区按比例仍过大——压缩目标 = 摘要 + 最小续工上下文。
|
|
513
|
+
const keepRatio = opts.force
|
|
514
|
+
? DEFAULT_BUDGET_POLICY.compactForceKeepRatio
|
|
515
|
+
: DEFAULT_BUDGET_POLICY.compactKeepRatio;
|
|
516
|
+
const keepBudget = Math.min(Math.floor(opts.window * keepRatio), DEFAULT_BUDGET_POLICY.compactKeepMaxTokens);
|
|
281
517
|
const kept = [];
|
|
282
518
|
let keptTokens = 0;
|
|
283
519
|
for (let k = groups.length - 1; k >= 0; k--) {
|
|
@@ -290,16 +526,31 @@ export async function compactHistory(history, opts) {
|
|
|
290
526
|
keptTokens = nextTokens;
|
|
291
527
|
}
|
|
292
528
|
let oldGroups = groups.slice(0, groups.length - kept.length);
|
|
529
|
+
// 非空 user 兜底:保留区从尾部累积,长回合里最近的若干组常常全是 assistant+tool,
|
|
530
|
+
// 一个 8000 字符的工具结果就能吃满小窗口的 keepBudget → user 全落进旧区被摘要掉 →
|
|
531
|
+
// 重建后 history 无 user → 下一步 chat() 被 hasNonEmptyUser 守卫直接拒(整轮中断)。
|
|
532
|
+
// 把紧邻保留区之前的那条 user 移进保留区:它是当前任务的原始请求,且 user 消息本身极小。
|
|
533
|
+
// 放这里(而非重建后补 stub)是因为:原文保留,不造新消息形状,也不必进摘要器。
|
|
534
|
+
if (!kept.some(isUserGroup)) {
|
|
535
|
+
for (let i = oldGroups.length - 1; i >= 0; i--) {
|
|
536
|
+
if (!isUserGroup(oldGroups[i]))
|
|
537
|
+
continue;
|
|
538
|
+
kept.unshift(oldGroups[i]);
|
|
539
|
+
oldGroups.splice(i, 1);
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
293
543
|
// force(硬闸/手动强压):保护区不豁免——常规切分无旧区时只保最后一组,
|
|
294
544
|
// 其余全部进可压区(首轮/当前轮也一样)。仍按 group 边界切,不破坏 tool_call 配对。
|
|
295
545
|
// **必须保留最早 user 所在 group**:LLM API(OpenAI / Anthropic)要求 messages 至少
|
|
296
546
|
// 含一条非空 user 消息,否则 400。force 旧实现把所有 user 丢进摘要 → 重建后 history
|
|
297
547
|
// 无 user → 下一轮 chat() 被后端拒绝。保最早 user(而非最后一个)因为它是最原始的
|
|
298
|
-
//
|
|
548
|
+
// 请求上下文,摘要器已覆盖后续交互。用 isUserGroup(非空判定)而非只看 role:
|
|
549
|
+
// 空 content 的 user 同样过不了 hasNonEmptyUser 守卫。
|
|
299
550
|
if (oldGroups.length === 0 && opts.force && groups.length >= 2) {
|
|
300
551
|
kept.length = 0;
|
|
301
552
|
const lastIdx = groups.length - 1;
|
|
302
|
-
const firstUserIdx = groups.findIndex(
|
|
553
|
+
const firstUserIdx = groups.findIndex(isUserGroup);
|
|
303
554
|
if (firstUserIdx >= 0 && firstUserIdx !== lastIdx) {
|
|
304
555
|
kept.push(groups[firstUserIdx], groups[lastIdx]);
|
|
305
556
|
oldGroups = groups.filter((_, i) => i !== firstUserIdx && i !== lastIdx);
|
|
@@ -360,35 +611,62 @@ export async function compactHistory(history, opts) {
|
|
|
360
611
|
}
|
|
361
612
|
return { ...noop, reason: 'noop-protected', protectedRatio };
|
|
362
613
|
}
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
let
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
614
|
+
// 主层:摘要——把旧区【原始内容】压成一条 system 摘要。
|
|
615
|
+
// 触发压缩时旧区约占窗口 50-60%,摘要请求与摘要器共享窗口,逐条封顶后即装得下;
|
|
616
|
+
// 不在这里预跑微压缩——预截断会让摘要模型看到 600 字符残片,等于弄瞎它,
|
|
617
|
+
// 且摘要成功后旧区整体丢弃,预截断本身也无收益。
|
|
618
|
+
// 上一代摘要的叙述段并入转录:它压的就是更早的轮次,不并进来等于把那些轮次整段丢弃;
|
|
619
|
+
// 并进来则由本代摘要统一重压(可重建内容,磨掉不心疼)。Key Facts 不并——它已被钉住。
|
|
620
|
+
let older = flattenGroups(oldGroups);
|
|
621
|
+
if (pinned?.rest) {
|
|
622
|
+
older = [
|
|
623
|
+
{
|
|
624
|
+
role: 'system',
|
|
625
|
+
content: `${PRIOR_NARRATIVE_LABEL}\n${pinned.rest}`,
|
|
626
|
+
},
|
|
627
|
+
...older,
|
|
628
|
+
];
|
|
373
629
|
}
|
|
374
|
-
// 第二层:摘要——把旧区(微压缩后)压成一条 system 摘要
|
|
375
|
-
const older = flattenGroups(oldGroups);
|
|
376
630
|
const summarizeFn = opts.summarize ?? defaultSummarize;
|
|
377
631
|
let summary = null;
|
|
378
632
|
try {
|
|
379
|
-
summary = await summarizeFn(older, opts.focus);
|
|
633
|
+
summary = await summarizeFn(older, opts.focus, opts.signal);
|
|
380
634
|
}
|
|
381
|
-
catch {
|
|
382
|
-
|
|
635
|
+
catch (err) {
|
|
636
|
+
// **中断必须冒泡,不能降级成微压缩**:用户按了 Ctrl+C 是要立刻停,不是"换个姿势继续压"。
|
|
637
|
+
// 冒泡后由 runAgentCore 步骤 catch 里的 abort 分支走 abortRestore(还原 history + 模式)。
|
|
638
|
+
// 此处 history 尚未被改动(重建在摘要成功之后),所以直接抛是安全的。
|
|
639
|
+
if (isAbortError(err, opts.signal))
|
|
640
|
+
throw err;
|
|
641
|
+
summary = null; // 真·摘要失败 → 回退微压缩兜底,不崩
|
|
383
642
|
}
|
|
384
643
|
if (summary) {
|
|
385
644
|
const artifactRefs = collectArtifactRefs(older);
|
|
386
645
|
const provenance = artifactRefs.length > 0
|
|
387
646
|
? `\n\n[artifact refs: ${artifactRefs.join(', ')}]`
|
|
388
647
|
: '';
|
|
648
|
+
// 钉住的 Key Facts 拼在段尾:单条 system 消息里尾部注意力最强,老事实不会被"读漏"。
|
|
649
|
+
// 恒为单条——并排多条 system 摘要会触发近因效应,早期摘要形同虚设。
|
|
650
|
+
const parsed = splitSummaryText(summary);
|
|
651
|
+
const pinnedFacts = mergeKeyFacts(pinned?.keyFacts, parsed.keyFacts, keyFactsBudgetChars(opts.window));
|
|
652
|
+
// P2:把 Objective/In Progress/Next Steps 固结到 notes.md——压缩那一刻
|
|
653
|
+
// notes.md 就有当前进度的权威副本,压缩后恢复提示据此续工,不再只依赖
|
|
654
|
+
// 模型自觉 plan_update。仅主 agent(共享 contextState)写;子 agent 独立
|
|
655
|
+
// contextState 不应污染主会话笔记。无活跃 plan 时写入(有则权威计划仍在)。
|
|
656
|
+
if (state === contextState) {
|
|
657
|
+
const snapshot = extractProgressSnapshot(parsed.rest);
|
|
658
|
+
if (snapshot)
|
|
659
|
+
writeCompactionSnapshot(snapshot);
|
|
660
|
+
}
|
|
661
|
+
const body = [
|
|
662
|
+
parsed.rest,
|
|
663
|
+
pinnedFacts ? `## Key Facts\n${pinnedFacts}` : '',
|
|
664
|
+
]
|
|
665
|
+
.filter(Boolean)
|
|
666
|
+
.join('\n\n');
|
|
389
667
|
const summaryMsg = {
|
|
390
668
|
role: 'system',
|
|
391
|
-
content:
|
|
669
|
+
content: `${SUMMARY_MARKER}\n${body}${provenance}`,
|
|
392
670
|
};
|
|
393
671
|
// 原地重建:[systemPrompt, summaryMsg, ...kept]
|
|
394
672
|
const systemMsg = history[0];
|
|
@@ -417,7 +695,15 @@ export async function compactHistory(history, opts) {
|
|
|
417
695
|
reason: 'summarize',
|
|
418
696
|
};
|
|
419
697
|
}
|
|
420
|
-
//
|
|
698
|
+
// 摘要失败兜底:微压缩——旧区原地截短(保 tool_call_id,无 LLM 调用),结构不动。
|
|
699
|
+
// 覆盖三类大字段,均只裁模型/工具产物,不动 user 原话与 system:
|
|
700
|
+
// ① tool 结果 content;② 旧 assistant 的 tool_calls.arguments(provenance stub,
|
|
701
|
+
// 整体超长才进,大字段替换为 "<N 字符,已省略>",保 path + JSON 合法);③ 旧 assistant 正文。
|
|
702
|
+
let microcompactDone = false;
|
|
703
|
+
for (const g of oldGroups) {
|
|
704
|
+
if (microcompactGroup(g))
|
|
705
|
+
microcompactDone = true;
|
|
706
|
+
}
|
|
421
707
|
const estimateAfter = estimatePromptTokens(history, activeTools, state.correction);
|
|
422
708
|
state.lastEstimate = estimateAfter;
|
|
423
709
|
state.lastUsage = undefined; // token 数已变,旧 usage 失效
|
|
@@ -460,7 +746,9 @@ export async function compactHistory(history, opts) {
|
|
|
460
746
|
* 强制走 compactHistory(manual/force 参数透传)。返 CompactResult 给 caller 文案展示。
|
|
461
747
|
* 默认 manual=false 自动路径完全不变。
|
|
462
748
|
*/
|
|
463
|
-
export async function maybeCompact(history, report, manualOpts, state = contextState, activeTools = chatTools
|
|
749
|
+
export async function maybeCompact(history, report, manualOpts, state = contextState, activeTools = chatTools,
|
|
750
|
+
/** 主 agent 的 abort signal;透传给 compactHistory → 摘要器 → chat()。 */
|
|
751
|
+
signal) {
|
|
464
752
|
const est = estimatePromptTokens(history, activeTools, state.correction);
|
|
465
753
|
state.lastEstimate = est;
|
|
466
754
|
const isManual = manualOpts?.manual === true;
|
|
@@ -497,6 +785,7 @@ export async function maybeCompact(history, report, manualOpts, state = contextS
|
|
|
497
785
|
force: manualOpts?.force === true || hardCap,
|
|
498
786
|
tools: activeTools,
|
|
499
787
|
contextState: state,
|
|
788
|
+
signal,
|
|
500
789
|
});
|
|
501
790
|
return r;
|
|
502
791
|
}
|
package/dist/session/notes.js
CHANGED
|
@@ -116,11 +116,17 @@ const NOTE_SECTION_TITLES = {
|
|
|
116
116
|
decisions: 'Decisions',
|
|
117
117
|
open_questions: 'Open Questions',
|
|
118
118
|
risks: 'Risks',
|
|
119
|
+
// compaction_snapshot 是压缩成功后自动写入的机器快照(非 note_append 手填):
|
|
120
|
+
// 让摘要的 Objective/In Progress/Next Steps 在压缩后仍作为活跃笔记段注入。
|
|
121
|
+
compaction_snapshot: 'Compaction Snapshot',
|
|
119
122
|
};
|
|
120
123
|
/** 预设段 key 列表(供工具 schema enum 与校验用)。 */
|
|
121
124
|
export const NOTE_SECTION_KEYS = Object.keys(NOTE_SECTION_TITLES);
|
|
122
125
|
/** 段注入优先级:数值越大越先占预算、越后丢弃正文。Risks 最重要。 */
|
|
123
126
|
const SECTION_PRIORITY = {
|
|
127
|
+
// compaction_snapshot 提到最高:它是压缩当次的权威进度快照,比手填 findings 更能
|
|
128
|
+
// 直接告诉模型「做到哪了」,压缩后第一步最该看到的就是它。
|
|
129
|
+
compaction_snapshot: 5,
|
|
124
130
|
risks: 4, findings: 3, decisions: 2, open_questions: 1,
|
|
125
131
|
};
|
|
126
132
|
/** 常驻笔记正文总预算(token)。5k:占百万级 context 的 0.5%,可常驻相当量笔记。 */
|
|
@@ -338,3 +344,60 @@ export function extractActiveNotesSections(budget = NOTES_INJECT_BUDGET_TOKENS,
|
|
|
338
344
|
}
|
|
339
345
|
return out.join('\n\n');
|
|
340
346
|
}
|
|
347
|
+
// ── Compaction Snapshot(压缩时自动固结的进度快照)──────────────────────────
|
|
348
|
+
// 由 compactHistory 在摘要成功后写入;替代「模型需自觉 plan_update」的软约定——
|
|
349
|
+
// 压缩那一刻 notes.md 里一定有当前进度的权威副本,压缩后恢复提示据此续工。
|
|
350
|
+
// 与 plan/notes 不同:这是机器产出、整段替换(不逐条累积),因此不会跨压缩膨胀。
|
|
351
|
+
/** 快照段标题。extractActiveNotesSections 经 matchSectionKey 识别并注入。 */
|
|
352
|
+
export const COMPACTION_SNAPSHOT_TITLE = NOTE_SECTION_TITLES.compaction_snapshot;
|
|
353
|
+
/**
|
|
354
|
+
* 把压缩摘要的关键段固结到 notes.md 的 `## Compaction Snapshot` 段。
|
|
355
|
+
* 整段替换旧快照(不累积);仅在当前无活跃 plan 时写入(已有 plan 时权威计划仍在,
|
|
356
|
+
* 快照只会重复)。body 为空时不动。永不抛错:压缩主流程不能因快照失败而失败。
|
|
357
|
+
*/
|
|
358
|
+
export function writeCompactionSnapshot(body, sessionId = getCurrentSessionId()) {
|
|
359
|
+
try {
|
|
360
|
+
const trimmed = (body ?? '').trim();
|
|
361
|
+
if (!trimmed)
|
|
362
|
+
return;
|
|
363
|
+
if (readActivePlanTitle(sessionId))
|
|
364
|
+
return; // 已有权威计划,快照是冗余
|
|
365
|
+
const p = getNotesFilePath(sessionId);
|
|
366
|
+
if (!p)
|
|
367
|
+
return;
|
|
368
|
+
let existing = '';
|
|
369
|
+
try {
|
|
370
|
+
existing = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
existing = '';
|
|
374
|
+
}
|
|
375
|
+
const header = `## ${COMPACTION_SNAPSHOT_TITLE}`;
|
|
376
|
+
const newSection = `${header}\n${trimmed}`;
|
|
377
|
+
const lines = existing.split('\n');
|
|
378
|
+
const start = lines.findIndex((l) => l.trim() === header);
|
|
379
|
+
let next;
|
|
380
|
+
if (start >= 0) {
|
|
381
|
+
// 段末 = 下一个 ## 或文件末;整段替换
|
|
382
|
+
let end = lines.length;
|
|
383
|
+
for (let k = start + 1; k < lines.length; k++) {
|
|
384
|
+
if (/^##\s/.test(lines[k])) {
|
|
385
|
+
end = k;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const before = lines.slice(0, start).join('\n').replace(/\s+$/, '');
|
|
390
|
+
const after = lines.slice(end).join('\n').replace(/^\s+/, '');
|
|
391
|
+
next = [before, newSection, after].filter((s) => s.length > 0).join('\n\n') + '\n';
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
const rest = existing.trim();
|
|
395
|
+
next = rest ? `${rest}\n\n${newSection}\n` : `${newSection}\n`;
|
|
396
|
+
}
|
|
397
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
398
|
+
fs.writeFileSync(p, next, 'utf8');
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// 快照是 best-effort 增强,绝不影响压缩主流程。
|
|
402
|
+
}
|
|
403
|
+
}
|
|
@@ -4,8 +4,6 @@ import { evaluateBudget, scheduleActions, formatReport, DEFAULT_BUDGET_POLICY, }
|
|
|
4
4
|
import { chatTools } from '../llm/index.js';
|
|
5
5
|
import { config } from '../config/index.js';
|
|
6
6
|
import { maybeCompact, contextState } from './compact.js';
|
|
7
|
-
import * as layout from '../ui/layout.js';
|
|
8
|
-
import { ui } from '../ui/theme.js';
|
|
9
7
|
import { pruneStaleArtifacts, refreshArtifactFreshness } from '../context/artifacts.js';
|
|
10
8
|
import { pruneSuperseded } from '../context/relevance.js';
|
|
11
9
|
import { createAgeAwareEncodingState } from '../context/age-aware.js';
|
|
@@ -28,7 +26,7 @@ export function createBudgetScheduler(state = contextState) {
|
|
|
28
26
|
const evaluate = (history, step, activeTools, ephemeralTokens) => evaluateBudget(history, config.contextWindowTokens, step, state.correction, activeTools, ephemeralTokens);
|
|
29
27
|
const scheduler = {
|
|
30
28
|
lastRunLog: null,
|
|
31
|
-
async runStep(history, step, activeTools = chatTools, ephemeralTokens = 0) {
|
|
29
|
+
async runStep(history, step, activeTools = chatTools, ephemeralTokens = 0, signal) {
|
|
32
30
|
// External file changes and mutations only update artifact metadata here.
|
|
33
31
|
refreshArtifactFreshness(state, history);
|
|
34
32
|
const report = evaluate(history, step, activeTools, ephemeralTokens);
|
|
@@ -54,11 +52,7 @@ export function createBudgetScheduler(state = contextState) {
|
|
|
54
52
|
let compactHistoryCalled = false;
|
|
55
53
|
let historyRebuilt = false;
|
|
56
54
|
for (const action of actions) {
|
|
57
|
-
|
|
58
|
-
layout.contentWrite(` ${ui.accent}●${ui.reset} ${ui.yellow}调度器警告 [${action.layer}] ${action.reason}${ui.reset}\n`);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const result = await maybeCompact(history, report, undefined, state, activeTools);
|
|
55
|
+
const result = await maybeCompact(history, report, undefined, state, activeTools, signal);
|
|
62
56
|
compactHistoryCalled = true;
|
|
63
57
|
historyRebuilt ||= result?.historyRebuilt === true;
|
|
64
58
|
}
|
|
@@ -82,6 +76,7 @@ export async function runScheduler(history, step, state = contextState, activeTo
|
|
|
82
76
|
}
|
|
83
77
|
/** User-requested compaction bypasses automatic pressure gating. */
|
|
84
78
|
export async function manualCompact(history, focus, opts) {
|
|
79
|
+
const signal = opts?.signal;
|
|
85
80
|
if (config.contextBudget === false) {
|
|
86
81
|
const result = await import('./compact.js').then(({ compactHistory }) => compactHistory(history, {
|
|
87
82
|
window: config.contextWindowTokens,
|
|
@@ -89,6 +84,7 @@ export async function manualCompact(history, focus, opts) {
|
|
|
89
84
|
focus,
|
|
90
85
|
manual: true,
|
|
91
86
|
force: opts?.force,
|
|
87
|
+
signal,
|
|
92
88
|
}));
|
|
93
89
|
const report = evaluateBudget(history, config.contextWindowTokens, -1, contextState.correction);
|
|
94
90
|
const log = {
|
|
@@ -127,7 +123,7 @@ export async function manualCompact(history, focus, opts) {
|
|
|
127
123
|
manual: true,
|
|
128
124
|
force: opts?.force,
|
|
129
125
|
focus: action.focus,
|
|
130
|
-
});
|
|
126
|
+
}, contextState, chatTools, signal);
|
|
131
127
|
compactHistoryCalled = true;
|
|
132
128
|
if (result) {
|
|
133
129
|
compactDetail = {
|
package/dist/tools/constants.js
CHANGED
|
@@ -10,6 +10,27 @@ export const MAX_HISTORY_RESULT = 8000;
|
|
|
10
10
|
export const MAX_SKILL_RESULT = 64000;
|
|
11
11
|
/** 微压缩时旧工具结果截到的存根长度(字符)。 */
|
|
12
12
|
export const MAX_OLD_TOOL_STUB = 600;
|
|
13
|
+
// ── 上下文压缩摘要(见 session/compact.ts defaultSummarize)────────────────
|
|
14
|
+
/** 摘要输入单条消息封顶:逐条封顶保证每轮对话都有代表,替代整段中截(会切掉中间整轮)。
|
|
15
|
+
* 转录总量超 SUMMARY_TRANSCRIPT_WINDOW_RATIO 时,这些封顶会被等比缩小重拼。 */
|
|
16
|
+
export const SUMMARY_MSG_MAX_CHARS = {
|
|
17
|
+
user: 3000, // 用户原话价值最高,给大额度
|
|
18
|
+
assistant: 1600,
|
|
19
|
+
tool: 800,
|
|
20
|
+
other: 2000,
|
|
21
|
+
};
|
|
22
|
+
/** 摘要转录总预算占上下文窗口的比例:超预算时等比缩小单条封顶重拼——
|
|
23
|
+
* 优先保消息条数(每轮都有代表),其次才保单条长度。 */
|
|
24
|
+
export const SUMMARY_TRANSCRIPT_WINDOW_RATIO = 0.55;
|
|
25
|
+
/** 摘要输出硬上限(字符):模型不听话产出超长摘要时按段落边界裁,防摘要本身撑大 history。 */
|
|
26
|
+
export const SUMMARY_OUTPUT_MAX_CHARS = 6000;
|
|
27
|
+
// ── 跨代钉住的 Key Facts 预算(见 compact.ts mergeKeyFacts)────────────────
|
|
28
|
+
/** 占窗口比例。按「字符≈token」的保守换算(中文 1 字≈1 token),取 summary 层的一半。 */
|
|
29
|
+
export const SUMMARY_KEYFACTS_WINDOW_RATIO = 0.05;
|
|
30
|
+
/** 小窗口下的绝对下限:首轮约束通常就几条,32k 窗口按比例只有 1.6k 也得保住。 */
|
|
31
|
+
export const SUMMARY_KEYFACTS_MIN_CHARS = 1200;
|
|
32
|
+
/** 大窗口下的绝对上限:Key Facts 是索引不是正文,不该无限膨胀。 */
|
|
33
|
+
export const SUMMARY_KEYFACTS_MAX_CHARS = 8000;
|
|
13
34
|
// ── 记忆(Tier-2 JSONL 工具库)──────────────────────────────────────────────
|
|
14
35
|
/** 单条记忆 body 上限(字符)。进 history 前 memory_search 结果另有 MAX_MEMORY_RESULT 兜底。 */
|
|
15
36
|
export const MAX_MEMORY_ENTRY = 4000;
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const TRACE_SECRET_MARKER_20260719 = 1;
|