mocode-ai 1.2.5 → 1.2.7
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/README.md +6 -6
- package/README.zh-CN.md +4 -4
- package/dist/agent/core.js +77 -37
- package/dist/agent/index.js +70 -34
- package/dist/agent/spawn.js +4 -0
- package/dist/commands/skill.js +230 -0
- package/dist/config/index.js +138 -12
- package/dist/config/presets.js +29 -7
- package/dist/context/artifacts.js +20 -0
- package/dist/context/budget.js +15 -6
- package/dist/context/encoders/table.js +1 -1
- package/dist/context/index.js +1 -1
- package/dist/host/stdio.js +10 -1
- package/dist/i18n/index.js +4 -4
- package/dist/llm/index.js +22 -3
- package/dist/llm/providers/anthropic.js +370 -0
- package/dist/memory/discover.js +8 -8
- package/dist/memory/index.js +3 -2
- package/dist/repl/index.js +94 -49
- package/dist/session/compact.js +24 -2
- package/dist/session/notes.js +233 -0
- package/dist/session/persist.js +2 -2
- package/dist/session/scheduler.js +4 -4
- package/dist/skills/runner.js +0 -1
- package/dist/skills/skill-eval.js +345 -0
- package/dist/skills/skill-improve.js +221 -0
- package/dist/skills/stats.js +102 -0
- package/dist/tools/builtins/index.js +4 -0
- package/dist/tools/builtins/note-append.js +103 -0
- package/dist/tools/registry.js +18 -5
- package/dist/ui/diff.js +1 -1
- package/dist/ui/layout.js +12 -1
- package/dist/ui/render.js +7 -1
- package/dist/verification/prompt.js +55 -0
- package/package.json +4 -3
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// `mocode skill` CLI 子命令(skill 自进化 Phase 0/1 的用户入口)。
|
|
2
|
+
//
|
|
3
|
+
// mocode skill eval <name> [--runs N] [--threshold x] [--timeout ms]
|
|
4
|
+
// 触发评测:对 <skill-dir>/evals/trigger.json 的每条 query 跑单轮 agent,
|
|
5
|
+
// 统计 use_skill/run_skill 触发率,输出 PASS/FAIL 报告并落盘 JSON。
|
|
6
|
+
//
|
|
7
|
+
// mocode skill improve <name> [--runs N] [--threshold x] [--iterations N]
|
|
8
|
+
// [--holdout x] [--timeout ms] [--apply]
|
|
9
|
+
// description 进化循环:train/holdout 切分 + LLM 提案 + 接受门,默认 dry-run
|
|
10
|
+
// 只打印最佳 description;--apply 才写回 SKILL.md(写前显式确认;非 TTY 拒绝)。
|
|
11
|
+
//
|
|
12
|
+
// mocode skill usage [name]
|
|
13
|
+
// 展示使用台账(Phase 0):按 skill 的调用次数/成功率/最近失败。
|
|
14
|
+
//
|
|
15
|
+
// 由 index.ts 在 `mocode skill …` 时动态加载(与 mocode config 同模式):
|
|
16
|
+
// 此时才引入 config 单例(LLM 配置)+ agent 依赖图,缺 LLM 配置时给友好报错而非崩。
|
|
17
|
+
// 纯打印/非 TUI:本进程不进 alt screen,输出走 stdout。
|
|
18
|
+
import * as readline from 'node:readline';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { config, isModelConfigured } from '../config/index.js';
|
|
21
|
+
import { listSkills } from '../skills/index.js';
|
|
22
|
+
import { loadSkillForEval, loadTriggerEvalSet, triggerEvalTemplate, triggerEvalPath, runTriggerEval, renderTriggerReport, saveTriggerReport, validateEvalParams, } from '../skills/skill-eval.js';
|
|
23
|
+
import { runImproveLoop, applyImprovedDescription } from '../skills/skill-improve.js';
|
|
24
|
+
import { loadSkillUsage, aggregateSkillStats, skillStatsPath } from '../skills/stats.js';
|
|
25
|
+
function usage() {
|
|
26
|
+
console.log(`mocode skill — skill 自进化(触发评测 / description 进化 / 使用台账)
|
|
27
|
+
|
|
28
|
+
用法:
|
|
29
|
+
mocode skill eval <name> [选项] 触发评测(需要 <skill-dir>/evals/trigger.json)
|
|
30
|
+
mocode skill improve <name> [选项] description 进化循环(默认 dry-run)
|
|
31
|
+
mocode skill usage [name] 展示使用台账
|
|
32
|
+
|
|
33
|
+
eval 选项:
|
|
34
|
+
--runs <n> 每 query 运行次数(默认 3;1..10)
|
|
35
|
+
--threshold <x> 触发率判定阈值(默认 0.5;>0 且 ≤1)
|
|
36
|
+
--timeout <ms> 单 query 超时(默认 60000)
|
|
37
|
+
|
|
38
|
+
improve 选项:
|
|
39
|
+
--runs <n> 同 eval(默认 3)
|
|
40
|
+
--threshold <x> 同 eval(默认 0.5)
|
|
41
|
+
--iterations <n> 迭代上限(默认 5)
|
|
42
|
+
--holdout <x> holdout 比例(默认 0.4;0 禁用)
|
|
43
|
+
--timeout <ms> 单 query 超时(默认 60000)
|
|
44
|
+
--apply 把最佳 description 写回 SKILL.md(默认 dry-run,仅打印)
|
|
45
|
+
|
|
46
|
+
eval 集格式(<skill-dir>/evals/trigger.json,非空数组):
|
|
47
|
+
[
|
|
48
|
+
{ "query": "应该触发该 skill 的真实请求", "should_trigger": true },
|
|
49
|
+
{ "query": "不应触发的相近请求", "should_trigger": false }
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
注: 进化对象仅限 ~/.mocode/skills 与 <cwd>/.mocode/skills 下的 skill(内置 skill 不可进化)。
|
|
53
|
+
没有 evals/trigger.json 的 skill 只能 eval 前手工补建,improve 一律拒绝(没验证门不优化)。`);
|
|
54
|
+
}
|
|
55
|
+
function numArg(args, name, fallback) {
|
|
56
|
+
const i = args.indexOf(name);
|
|
57
|
+
if (i === -1 || i + 1 >= args.length)
|
|
58
|
+
return fallback;
|
|
59
|
+
const v = Number(args[i + 1]);
|
|
60
|
+
if (!Number.isFinite(v) || v <= 0) {
|
|
61
|
+
throw new Error(`${name} 需要正数: ${args[i + 1]}`);
|
|
62
|
+
}
|
|
63
|
+
return v;
|
|
64
|
+
}
|
|
65
|
+
function parseCommon(args) {
|
|
66
|
+
const runs = Math.round(numArg(args, '--runs', 3));
|
|
67
|
+
const threshold = numArg(args, '--threshold', 0.5);
|
|
68
|
+
const timeoutMs = Math.round(numArg(args, '--timeout', 60_000));
|
|
69
|
+
const err = validateEvalParams(runs, threshold);
|
|
70
|
+
if (err)
|
|
71
|
+
throw new Error(err);
|
|
72
|
+
return { runs, threshold, timeoutMs };
|
|
73
|
+
}
|
|
74
|
+
function requireModel() {
|
|
75
|
+
if (!isModelConfigured()) {
|
|
76
|
+
console.error('未配置 LLM(缺 LLM_BASE_URL / LLM_API_KEY)。先运行 `mocode config` 或设置环境变量。');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** 定位 eval 集;缺失时打印模板并退出(不猜、不自动建)。 */
|
|
81
|
+
function requireEvalSet(skillName) {
|
|
82
|
+
const skill = loadSkillForEval(skillName);
|
|
83
|
+
const cases = loadTriggerEvalSet(skill);
|
|
84
|
+
if (!cases) {
|
|
85
|
+
console.error(`未找到触发评测集: ${path.relative(process.cwd(), triggerEvalPath(skill))}`);
|
|
86
|
+
console.error('手工创建该文件(格式):');
|
|
87
|
+
console.error(triggerEvalTemplate(skill));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
return { skill, cases };
|
|
91
|
+
}
|
|
92
|
+
async function evalCommand(args) {
|
|
93
|
+
const rest = args.slice();
|
|
94
|
+
const name = rest.find((a) => !a.startsWith('--'));
|
|
95
|
+
if (!name)
|
|
96
|
+
throw new Error('缺少 skill 名。见: mocode skill eval --help');
|
|
97
|
+
requireModel();
|
|
98
|
+
const { skill, cases } = requireEvalSet(name);
|
|
99
|
+
const { runs, threshold, timeoutMs } = parseCommon(rest);
|
|
100
|
+
console.log(`评测 skill "${skill.name}" — ${cases.length} 条 query × ${runs} 次,模型 ${config.model}\n`);
|
|
101
|
+
const startedAt = Date.now();
|
|
102
|
+
const report = await runTriggerEval(skill, skill.description, cases, runs, threshold, {
|
|
103
|
+
timeoutMs,
|
|
104
|
+
onProgress: (_d, _t, line) => process.stdout.write(line + '\n'),
|
|
105
|
+
});
|
|
106
|
+
const saved = saveTriggerReport(report);
|
|
107
|
+
console.log(`\n${renderTriggerReport(report, { runsPerQuery: runs, threshold })}`);
|
|
108
|
+
console.log(`\n耗时 ${((Date.now() - startedAt) / 1000).toFixed(1)}s,结果已存: ${path.relative(process.cwd(), saved)}`);
|
|
109
|
+
if (report.summary.passed < report.summary.total) {
|
|
110
|
+
console.log('存在失败项。可用 `mocode skill improve ' + skill.name + '` 尝试自动优化 description(dry-run)。');
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function confirmApply(skillName) {
|
|
115
|
+
if (!process.stdin.isTTY) {
|
|
116
|
+
// 非 TTY fail closed:自动落盘必须有人在场。
|
|
117
|
+
console.error('非 TTY 环境拒绝 --apply 自动落盘。请在终端运行,或手工编辑 SKILL.md 的 description。');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
rl.question(`确认把新 description 写回 skill "${skillName}" 的 SKILL.md?(y/N) `, (ans) => {
|
|
123
|
+
rl.close();
|
|
124
|
+
resolve(ans.trim().toLowerCase() === 'y' || ans.trim().toLowerCase() === 'yes');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function improveCommand(args) {
|
|
129
|
+
const rest = args.slice();
|
|
130
|
+
const name = rest.find((a) => !a.startsWith('--'));
|
|
131
|
+
if (!name)
|
|
132
|
+
throw new Error('缺少 skill 名。见: mocode skill improve --help');
|
|
133
|
+
requireModel();
|
|
134
|
+
const { skill, cases } = requireEvalSet(name);
|
|
135
|
+
const { runs, threshold, timeoutMs } = parseCommon(rest);
|
|
136
|
+
const iterations = Math.round(numArg(rest, '--iterations', 5));
|
|
137
|
+
const holdout = (() => {
|
|
138
|
+
const i = rest.indexOf('--holdout');
|
|
139
|
+
const v = i === -1 ? 0.4 : Number(rest[i + 1]);
|
|
140
|
+
if (i !== -1 && (!Number.isFinite(v) || v < 0 || v > 0.9))
|
|
141
|
+
throw new Error('--holdout 需要 0..0.9 的数');
|
|
142
|
+
return v;
|
|
143
|
+
})();
|
|
144
|
+
const apply = rest.includes('--apply');
|
|
145
|
+
console.log(`进化 skill "${skill.name}" — ${cases.length} 条 query,train/holdout=${1 - holdout}/${holdout},${apply ? '将写回(--apply)' : 'dry-run(不落盘)'}\n`);
|
|
146
|
+
const result = await runImproveLoop(skill, cases, {
|
|
147
|
+
maxIterations: iterations,
|
|
148
|
+
runsPerQuery: runs,
|
|
149
|
+
threshold,
|
|
150
|
+
holdout,
|
|
151
|
+
timeoutMs,
|
|
152
|
+
onIteration: (_i, _max, line) => console.log(line),
|
|
153
|
+
});
|
|
154
|
+
console.log('');
|
|
155
|
+
if (!result.improved) {
|
|
156
|
+
console.log(`没有更优的 description(原 description 已是最佳或循环未产出改进)。`);
|
|
157
|
+
console.log(`原: ${result.originalDescription}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log(`原 description: ${result.originalDescription}`);
|
|
161
|
+
console.log(`新 description: ${result.bestDescription}`);
|
|
162
|
+
if (apply) {
|
|
163
|
+
const ok = await confirmApply(skill.name);
|
|
164
|
+
if (!ok) {
|
|
165
|
+
console.log('已取消写回(dry-run 结果见上,可手工应用)。');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
applyImprovedDescription(skill, result.bestDescription);
|
|
169
|
+
console.log(`已写回: ${skill.skillMdPath}`);
|
|
170
|
+
console.log('提示: 内容哈希已变更,project 级 skill 下次 run_skill 会重新要求信任确认。');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.log('dry-run:未写文件。确认满意后加 --apply 重跑,或直接手工把新 description 写进 SKILL.md。');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function usageCommand(args) {
|
|
177
|
+
const filter = args.find((a) => !a.startsWith('--'));
|
|
178
|
+
const records = loadSkillUsage();
|
|
179
|
+
const summaries = aggregateSkillStats(records);
|
|
180
|
+
const list = filter ? summaries.filter((s) => s.skill === filter) : summaries;
|
|
181
|
+
if (list.length === 0) {
|
|
182
|
+
console.log(`(台账为空: ${path.relative(process.cwd(), skillStatsPath())})`);
|
|
183
|
+
console.log('台账在模型调用 use_skill / run_skill 时自动记录,跑几个会话后这里有数。');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (const s of list) {
|
|
187
|
+
const rate = s.runSuccessRate === null ? '' : ` run 成功率 ${(s.runSuccessRate * 100).toFixed(0)}%`;
|
|
188
|
+
console.log(`${s.skill} ×${s.total}(use ${s.uses} / run ${s.runs})${rate} 最近 ${s.lastUsedAt}`);
|
|
189
|
+
if (s.lastFailure) {
|
|
190
|
+
console.log(` 最近失败: ${s.lastFailure.status}${s.lastFailure.code ? ` (${s.lastFailure.code})` : ''} @ ${s.lastFailure.ts}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export async function runSkillCommand(args) {
|
|
195
|
+
const sub = args[0];
|
|
196
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
197
|
+
usage();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
if (sub === 'eval')
|
|
202
|
+
await evalCommand(args.slice(1));
|
|
203
|
+
else if (sub === 'improve')
|
|
204
|
+
await improveCommand(args.slice(1));
|
|
205
|
+
else if (sub === 'usage')
|
|
206
|
+
usageCommand(args.slice(1));
|
|
207
|
+
else {
|
|
208
|
+
console.error(`未知子命令 "${sub}"。`);
|
|
209
|
+
usage();
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
215
|
+
if (/aborted/i.test(msg)) {
|
|
216
|
+
console.error('\n已中断。');
|
|
217
|
+
process.exitCode = 130;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.error(`错误: ${msg}`);
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 供 index.ts 判断子命令名集合(避免它 parse 我们的参数)。
|
|
226
|
+
export const SKILL_SUBCOMMANDS = ['eval', 'improve', 'usage', 'help', '--help', '-h'];
|
|
227
|
+
// listSkills 在 eval/improve 未命中时供报错文案复用(保持与 /skills 一致的名字集)。
|
|
228
|
+
export function listSkillNames() {
|
|
229
|
+
return listSkills().map((s) => s.name);
|
|
230
|
+
}
|
package/dist/config/index.js
CHANGED
|
@@ -3,8 +3,9 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
5
|
import { getCurrentSessionId } from '../session/state.js';
|
|
6
|
-
import { getNotesFilePath } from '../session/notes.js';
|
|
6
|
+
import { getNotesFilePath, extractActiveNotesSections } from '../session/notes.js';
|
|
7
7
|
import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
|
|
8
|
+
import { buildValidationCommandsSection } from '../verification/prompt.js';
|
|
8
9
|
import { detectLanguage, setLanguage, t, } from '../i18n/index.js';
|
|
9
10
|
/**
|
|
10
11
|
* 按优先级加载配置文件并回填 process.env:
|
|
@@ -40,11 +41,14 @@ export const languageFromShell = process.env.MOCODE_LANGUAGE !== undefined;
|
|
|
40
41
|
// 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
|
|
41
42
|
// 仿 themeFromShell 模式:shell export 的环境变量在 loadEnvFiles 中不被回填(优先级最高),
|
|
42
43
|
// 故 /model 写入 ~/.mocode/config 的同名键下次启动会被 shell 值覆盖——据此给 dim 警告。
|
|
43
|
-
const LLM_ENV_KEYS = ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS'];
|
|
44
|
+
const LLM_ENV_KEYS = ['LLM_PROVIDER', 'LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS', 'ANTHROPIC_PROMPT_CACHE'];
|
|
44
45
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 256000;
|
|
45
46
|
const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
|
|
46
47
|
loadEnvFiles();
|
|
47
48
|
setLanguage(detectLanguage(process.env.MOCODE_LANGUAGE));
|
|
49
|
+
export function normalizeLlmProvider(value) {
|
|
50
|
+
return typeof value === 'string' && value.toLowerCase() === 'anthropic' ? 'anthropic' : 'openai';
|
|
51
|
+
}
|
|
48
52
|
/**
|
|
49
53
|
* 取环境变量;缺则返回空字符串(不退出)。
|
|
50
54
|
* 历史上缺 LLM_BASE_URL/LLM_API_KEY 会 process.exit(1),但 /model 命令已能在 REPL 内配置模型,
|
|
@@ -214,20 +218,109 @@ export function reinjectActivePlanIntoSystem(history) {
|
|
|
214
218
|
sys.content = `${content}${ACTIVE_PLAN_MARKER}${plan}\n`;
|
|
215
219
|
return true;
|
|
216
220
|
}
|
|
221
|
+
/** compact 重注入用的幂等标记:history[0] 中夹住会话笔记段正文(Findings/Decisions/Open Questions/Risks),
|
|
222
|
+
* 重复注入只替换不累积。与 ACTIVE_PLAN_MARKER 独立,互不干扰。 */
|
|
223
|
+
const NOTES_BODY_MARKER = '\n\n<!-- mocode:session-notes -->\n';
|
|
224
|
+
/**
|
|
225
|
+
* 把会话笔记段正文重注入系统提示(history[0])。compact 后或本步改了 notes.md 时调用:
|
|
226
|
+
* 若 notes.md 有活跃笔记段(extractActiveNotesSections 返回非空,已按 5k token 预算裁剪),
|
|
227
|
+
* 则覆盖旧标记块写入最新内容;若无,则清掉残留标记块。直接改 history[0].content,
|
|
228
|
+
* 幂等,返回是否改动。与 reinjectActivePlanIntoSystem 独立:plan 段由后者管,笔记段由本函数管。
|
|
229
|
+
*/
|
|
230
|
+
export function reinjectSessionNotesIntoSystem(history) {
|
|
231
|
+
const sys = history[0];
|
|
232
|
+
if (!sys || sys.role !== 'system' || typeof sys.content !== 'string')
|
|
233
|
+
return false;
|
|
234
|
+
let content = sys.content;
|
|
235
|
+
const markerIdx = content.indexOf(NOTES_BODY_MARKER);
|
|
236
|
+
if (markerIdx >= 0) {
|
|
237
|
+
content = content.slice(0, markerIdx).replace(/\s+$/, '');
|
|
238
|
+
}
|
|
239
|
+
const notes = extractActiveNotesSections();
|
|
240
|
+
if (!notes) {
|
|
241
|
+
if (markerIdx < 0)
|
|
242
|
+
return false;
|
|
243
|
+
sys.content = content;
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
sys.content = `${content}${NOTES_BODY_MARKER}${notes}\n`;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 一次性重注入会话状态(plan 段 + 笔记段)到系统提示。返回任一 marker 是否改动。
|
|
251
|
+
*
|
|
252
|
+
* @deprecated 热路径已不再调用(#prompt-cache):往 history[0] 追加 plan/笔记会让
|
|
253
|
+
* 系统提示每次 plan_update / note_append 后变字节,前缀缓存整段失效(系统提示 6-8k token,
|
|
254
|
+
* 本轮后续每步全价重算)。现由 agent/core 每步在 requestHistory **末尾**注入
|
|
255
|
+
* {@link buildSessionStateReminder} 的 ephemeral system 消息:模型看到的信息等价,
|
|
256
|
+
* 但变动落在前缀末端。本函数仅留给外部集成 / 旧测试,新增调用点请勿使用。
|
|
257
|
+
*/
|
|
258
|
+
export function reinjectSessionStateIntoSystem(history) {
|
|
259
|
+
const planChanged = reinjectActivePlanIntoSystem(history);
|
|
260
|
+
const notesChanged = reinjectSessionNotesIntoSystem(history);
|
|
261
|
+
return planChanged || notesChanged;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 构造"会话状态提醒"正文(活跃 `## Plan:` 段 + 活跃笔记段正文),供 agent/core 每步
|
|
265
|
+
* 拼进 requestHistory **末尾**的 ephemeral system 消息。
|
|
266
|
+
*
|
|
267
|
+
* 为什么在尾部而不是 history[0](prompt 缓存):plan_update / note_append 是设计上鼓励
|
|
268
|
+
* 高频调用的工具,一旦它们改写系统提示,支持自动前缀缓存的后端(OpenAI / DeepSeek /
|
|
269
|
+
* GLM / Qwen)就会从第一个 token 起全部 miss。放到历史末尾后,前面整段(系统提示 + 全部
|
|
270
|
+
* 已有对话)保持逐字节稳定,只有尾部这一小条随 notes.md 变化。
|
|
271
|
+
*
|
|
272
|
+
* 纯读函数:不改 history,也不写文件。notes.md 不存在 / 无活跃内容时返回 ''(零开销)。
|
|
273
|
+
*/
|
|
274
|
+
export function buildSessionStateReminder(sessionId = getCurrentSessionId()) {
|
|
275
|
+
const plan = extractActivePlanSection(sessionId);
|
|
276
|
+
const notes = extractActiveNotesSections(undefined, sessionId);
|
|
277
|
+
if (!plan && !notes)
|
|
278
|
+
return '';
|
|
279
|
+
const parts = [
|
|
280
|
+
'## Session state (current, from notes.md)',
|
|
281
|
+
'This block mirrors the live session notepad and is refreshed every step; treat it as the authoritative plan/notes state, and ignore any older copy earlier in this conversation.',
|
|
282
|
+
...(plan ? [plan] : []),
|
|
283
|
+
...(notes ? [notes] : []),
|
|
284
|
+
];
|
|
285
|
+
return parts.join('\n\n');
|
|
286
|
+
}
|
|
217
287
|
const SYSTEM_PROMPT_MEMORY_SECTION = `
|
|
218
288
|
## Memory (cross-session facts)
|
|
219
289
|
- The prompt may contain a title/summary index; retrieve details with memory_search or inspect all with memory_list. memory_search also surfaces knowledge-graph facts (relations between entities) alongside entry bodies.
|
|
220
290
|
- Save only stable, non-obvious cross-session facts. Search before saving; update an existing entry instead of duplicating it, and archive stale entries.
|
|
221
291
|
- A knowledge-graph layer links entities across memories: explore relations/neighbors with memory_graph (neighbors/add/stats), and attach meaningful links via the links parameter of memory_save when saving.`;
|
|
222
|
-
/**
|
|
292
|
+
/** AGENTS.md 自动导入正文上限:system 位于 history[0] 且 compactHistory 不压缩 system,超长需截断防占窗口(见 memory/README.md)。 */
|
|
293
|
+
const MAX_AGENTS_IMPORT_CHARS = 20000;
|
|
294
|
+
/**
|
|
295
|
+
* 工作区根 AGENTS.md 自动导入段:与 memory 开关完全无关——
|
|
296
|
+
* 只要 <cwd>/AGENTS.md 存在就把正文直接拼进 prompt(超 {@link MAX_AGENTS_IMPORT_CHARS} 截断+末尾提示),
|
|
297
|
+
* 不再只指路让模型按需 read_file。读失败静默跳过(返空串)。
|
|
298
|
+
*/
|
|
299
|
+
function buildAgentsImportSection() {
|
|
300
|
+
try {
|
|
301
|
+
const projectAgents = path.join(process.cwd(), 'AGENTS.md');
|
|
302
|
+
if (!fs.existsSync(projectAgents))
|
|
303
|
+
return '';
|
|
304
|
+
const content = fs.readFileSync(projectAgents, 'utf8').trim();
|
|
305
|
+
if (!content)
|
|
306
|
+
return '';
|
|
307
|
+
const body = content.length > MAX_AGENTS_IMPORT_CHARS
|
|
308
|
+
? `${content.slice(0, MAX_AGENTS_IMPORT_CHARS)}\n…[AGENTS.md truncated: first ${MAX_AGENTS_IMPORT_CHARS} characters injected]`
|
|
309
|
+
: content;
|
|
310
|
+
return `\n## Project memory (AGENTS.md, auto-imported)\n${body}\n- AGENTS.md may be stale: current code and the user request override stale memory.`;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return ''; // 读失败静默跳过:不让导入破坏 prompt 构建
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Memory 检索指导段(与 AGENTS.md 导入无关):开 isMemoryEnabled() 时才拼,
|
|
318
|
+
* 注入 memory_search/list/graph 的使用指导。默认关(新用户零侵入)。
|
|
319
|
+
*/
|
|
223
320
|
function buildMemoryPromptSection() {
|
|
224
321
|
if (!isMemoryEnabled())
|
|
225
322
|
return '';
|
|
226
|
-
|
|
227
|
-
const mocodeHint = fs.existsSync(projectMocode)
|
|
228
|
-
? '\n- `MOCODE.md` exists at the workspace root but is not preloaded. Read it with `read_file` only when the task may depend on project architecture, conventions, commands, prior decisions, or user preferences; skip it for greetings and unrelated simple requests. Current code and the user request override stale memory.'
|
|
229
|
-
: '';
|
|
230
|
-
return SYSTEM_PROMPT_MEMORY_SECTION + mocodeHint;
|
|
323
|
+
return SYSTEM_PROMPT_MEMORY_SECTION;
|
|
231
324
|
}
|
|
232
325
|
/**
|
|
233
326
|
* plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
|
|
@@ -258,6 +351,7 @@ ${buildPlanResearchRules()}`;
|
|
|
258
351
|
}
|
|
259
352
|
/** 兼容旧名字:repl 的 buildSystemMessage 仍引 PLAN_MODE_SUFFIX(变量)。运行时按需现拼。 */
|
|
260
353
|
export function buildBasePrompt(sessionId = getCurrentSessionId()) {
|
|
354
|
+
const agentsImportSection = buildAgentsImportSection();
|
|
261
355
|
const memorySection = buildMemoryPromptSection();
|
|
262
356
|
const notepadSection = buildNotepadSection(sessionId);
|
|
263
357
|
// 静态主体:稳定段落集中在前,让支持 prompt caching 的后端能命中前缀缓存(#12)。
|
|
@@ -283,6 +377,7 @@ Complete programming tasks through an "analyze → call tool → observe result
|
|
|
283
377
|
- Report: stop when done and give honest conclusions with path:line references (see Reporting).
|
|
284
378
|
- Use web search only when freshness materially affects the answer.
|
|
285
379
|
${buildCodegraphSection()}
|
|
380
|
+
${buildValidationCommandsSection()}
|
|
286
381
|
|
|
287
382
|
## Engineering principles
|
|
288
383
|
${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
@@ -313,9 +408,9 @@ ${buildVoiceSection()}
|
|
|
313
408
|
- **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
|
|
314
409
|
- Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.
|
|
315
410
|
${t('assistant.languageInstruction')}`;
|
|
316
|
-
// 动态段(置于末尾):memory 索引 + notepad 索引 + notepad 使用说明。
|
|
411
|
+
// 动态段(置于末尾):AGENTS.md 项目记忆(无条件) + memory 索引(按开关) + notepad 索引 + notepad 使用说明。
|
|
317
412
|
// 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
|
|
318
|
-
// - "## Project context" 仅当 memorySection/notepadSection
|
|
413
|
+
// - "## Project context" 仅当 agentsImportSection/memorySection/notepadSection 任一非空(notepad 索引依赖 notes.md 存在);
|
|
319
414
|
// - "## Session state" 使用说明**无条件**注入(放在动态尾段首位):否则会陷入"说明依赖 notes.md 存在 → 模型不知要建 → 文件永不存在"的鸡生蛋循环,功能对模型不可见。动态段在静态前缀之后,不影响 prompt 缓存。
|
|
320
415
|
const dynamicParts = [];
|
|
321
416
|
// 会话级私有尾段(子 agent 切片会丢弃):Session state 说明无条件注入在前,Project context 按需在后。
|
|
@@ -333,8 +428,10 @@ ${t('assistant.languageInstruction')}`;
|
|
|
333
428
|
'Keep at most one step in_progress, and mark a step completed as soon as its work is done — do not batch updates to the end of the turn. ' +
|
|
334
429
|
'Write each step so a teammate who lost the conversation could pick it up cold: name the file or symbol, the exact change, and the verification, so the plan survives context compaction. ' +
|
|
335
430
|
'plan_update creates notes.md for you when the task warrants it; read_file the full notes.md whenever you need to recover context after compaction. ' +
|
|
336
|
-
'When every step is completed, plan_update settles the plan to `## Done:` automatically. Keep other notes concise and session-specific; use memory for stable cross-session facts
|
|
337
|
-
|
|
431
|
+
'When every step is completed, plan_update settles the plan to `## Done:` automatically. Keep other notes concise and session-specific; use memory for stable cross-session facts.\n' +
|
|
432
|
+
'## Session notes (resident memory)\n' +
|
|
433
|
+
'For non-obvious, lasting-value discoveries — subtle constraints, decisions with downstream impact, open questions blocking a choice, or risks affecting later steps — call `note_append` IMMEDIATELY when you make the discovery. The note is written to the same notes.md and its body is re-injected into the prompt automatically (within a 5k-token budget), surviving compaction so you keep remembering what you found/decided this session. Do NOT use it for routine progress (that is the plan) or stable cross-session facts (that is memory_save). Each call appends one item.');
|
|
434
|
+
const ctxContent = `${agentsImportSection}${memorySection}${notepadSection}`.trimEnd();
|
|
338
435
|
if (ctxContent) {
|
|
339
436
|
dynamicParts.push(`## Project context\n${ctxContent}`);
|
|
340
437
|
}
|
|
@@ -382,6 +479,7 @@ export function getPlanModeSuffix() {
|
|
|
382
479
|
return buildPlanModeSuffix();
|
|
383
480
|
}
|
|
384
481
|
export const config = {
|
|
482
|
+
provider: normalizeLlmProvider(process.env.LLM_PROVIDER),
|
|
385
483
|
baseURL: requireEnv('LLM_BASE_URL'),
|
|
386
484
|
apiKey: requireEnv('LLM_API_KEY'),
|
|
387
485
|
model: process.env.LLM_MODEL || 'gpt-4o-mini',
|
|
@@ -393,6 +491,7 @@ export const config = {
|
|
|
393
491
|
},
|
|
394
492
|
contextWindowTokens: Number(process.env.CONTEXT_WINDOW_TOKENS) || DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
395
493
|
includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
|
|
494
|
+
anthropicPromptCache: process.env.ANTHROPIC_PROMPT_CACHE !== 'false',
|
|
396
495
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
397
496
|
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE === 'true',
|
|
398
497
|
contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE === 'true',
|
|
@@ -418,6 +517,21 @@ export const config = {
|
|
|
418
517
|
permissionEnabled: process.env.MOCODE_PERMISSION !== 'false',
|
|
419
518
|
permissionNonInteractiveAllow: process.env.MOCODE_PERMISSION_NON_INTERACTIVE_ALLOW === 'true',
|
|
420
519
|
};
|
|
520
|
+
/**
|
|
521
|
+
* 会话钉死模型:窗口/会话启动时由 pinSessionModel() 捕获一次。
|
|
522
|
+
* 运行中 agent 一律经 getActiveModel() 取模型,而非热切的 config.model——
|
|
523
|
+
* 这样某窗口 /model switch 改写全局 config 后,其它【已经打开】的窗口的
|
|
524
|
+
* 运行 agent 仍用各自启动时的模型,不会被影响;只有重启/新开窗口才会读全局 config。
|
|
525
|
+
*/
|
|
526
|
+
let sessionModel = null;
|
|
527
|
+
/** 在 REPL 启动时调用一次,把当前模型钉成本会话的活跃模型。 */
|
|
528
|
+
export function pinSessionModel() {
|
|
529
|
+
sessionModel = config.model;
|
|
530
|
+
}
|
|
531
|
+
/** 运行中 agent 实际使用的模型:优先钉死值,未钉(极早路径)则回退 config.model。 */
|
|
532
|
+
export function getActiveModel() {
|
|
533
|
+
return sessionModel ?? config.model;
|
|
534
|
+
}
|
|
421
535
|
/**
|
|
422
536
|
* 运行时更新模型相关配置(/model 命令调)。
|
|
423
537
|
* - 更新 config 对象字段(即时生效:chat() 读 config.model,reconfigureClient 读 config.baseURL/apiKey)。
|
|
@@ -427,8 +541,16 @@ export const config = {
|
|
|
427
541
|
* 重建 OpenAI 客户端(baseURL/apiKey 是构造时固化的实例字段)由调用方走 reconfigureClient。
|
|
428
542
|
*/
|
|
429
543
|
export function updateModelConfig(opts) {
|
|
544
|
+
if (opts.provider !== undefined) {
|
|
545
|
+
config.provider = opts.provider;
|
|
546
|
+
process.env.LLM_PROVIDER = opts.provider;
|
|
547
|
+
}
|
|
430
548
|
if (opts.model !== undefined) {
|
|
431
549
|
config.model = opts.model;
|
|
550
|
+
// 钉死值同步更新:本窗口显式 /model switch 立即对本窗口运行 agent 生效;
|
|
551
|
+
// 其它已开窗口的 sessionModel 不受影响(各自启动时钉死)。
|
|
552
|
+
if (sessionModel !== null)
|
|
553
|
+
sessionModel = opts.model;
|
|
432
554
|
process.env.LLM_MODEL = opts.model;
|
|
433
555
|
}
|
|
434
556
|
if (opts.baseURL !== undefined) {
|
|
@@ -443,6 +565,10 @@ export function updateModelConfig(opts) {
|
|
|
443
565
|
config.contextWindowTokens = opts.contextWindowTokens;
|
|
444
566
|
process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
|
|
445
567
|
}
|
|
568
|
+
if (opts.anthropicPromptCache !== undefined) {
|
|
569
|
+
config.anthropicPromptCache = opts.anthropicPromptCache;
|
|
570
|
+
process.env.ANTHROPIC_PROMPT_CACHE = opts.anthropicPromptCache ? 'true' : 'false';
|
|
571
|
+
}
|
|
446
572
|
}
|
|
447
573
|
/** 子 Agent 总开关;默认 false,关闭时 sub-agent 不进入模型工具表。 */
|
|
448
574
|
export function isSubAgentEnabled() {
|
package/dist/config/presets.js
CHANGED
|
@@ -31,8 +31,8 @@ function filePathFor(name) {
|
|
|
31
31
|
}
|
|
32
32
|
return path.join(MODELS_DIR, `${name}.json`);
|
|
33
33
|
}
|
|
34
|
-
/** 把磁盘上的 raw JSON 解析并校验为 ModelPreset
|
|
35
|
-
function parsePreset(raw) {
|
|
34
|
+
/** 把磁盘上的 raw JSON 解析并校验为 ModelPreset;旧预设缺 provider 时按 openai 读取。 */
|
|
35
|
+
export function parsePreset(raw) {
|
|
36
36
|
const obj = JSON.parse(raw);
|
|
37
37
|
const { name, baseURL, apiKey, model, contextWindow } = obj;
|
|
38
38
|
if (typeof name !== 'string' || !isValidPresetName(name)) {
|
|
@@ -50,7 +50,17 @@ function parsePreset(raw) {
|
|
|
50
50
|
if (typeof contextWindow !== 'number' || !Number.isFinite(contextWindow) || contextWindow <= 0) {
|
|
51
51
|
throw new Error(`预设 ${name}: contextWindow 必须为正数`);
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
const provider = obj.provider === 'anthropic' ? 'anthropic' : 'openai';
|
|
54
|
+
const anthropicPromptCache = provider === 'anthropic' && obj.anthropicPromptCache !== false;
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
provider,
|
|
58
|
+
baseURL,
|
|
59
|
+
apiKey,
|
|
60
|
+
model,
|
|
61
|
+
contextWindow: Math.floor(contextWindow),
|
|
62
|
+
anthropicPromptCache,
|
|
63
|
+
};
|
|
54
64
|
}
|
|
55
65
|
/** 读单个预设;不存在抛错。 */
|
|
56
66
|
export function getPreset(name) {
|
|
@@ -68,15 +78,21 @@ export function readPreset(name) {
|
|
|
68
78
|
throw e;
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
|
-
/** 写/覆盖一个预设(原子:写 tmp 再 rename)
|
|
81
|
+
/** 写/覆盖一个预设(原子:写 tmp 再 rename)。旧调用缺 provider 时仍按 openai 保存。 */
|
|
72
82
|
export function savePreset(preset) {
|
|
73
83
|
if (!isValidPresetName(preset.name)) {
|
|
74
84
|
throw new Error(`非法预设名: ${JSON.stringify(preset.name)}`);
|
|
75
85
|
}
|
|
86
|
+
const provider = preset.provider ?? 'openai';
|
|
87
|
+
const normalized = {
|
|
88
|
+
...preset,
|
|
89
|
+
provider,
|
|
90
|
+
anthropicPromptCache: provider === 'anthropic' && preset.anthropicPromptCache !== false,
|
|
91
|
+
};
|
|
76
92
|
fs.mkdirSync(MODELS_DIR, { recursive: true });
|
|
77
93
|
const dest = filePathFor(preset.name);
|
|
78
94
|
const tmp = `${dest}.tmp-${process.pid}-${Date.now()}`;
|
|
79
|
-
fs.writeFileSync(tmp, JSON.stringify(
|
|
95
|
+
fs.writeFileSync(tmp, JSON.stringify(normalized, null, 2), 'utf8');
|
|
80
96
|
fs.renameSync(tmp, dest);
|
|
81
97
|
}
|
|
82
98
|
/** 删除一个预设;不存在返回 false,成功返回 true。 */
|
|
@@ -156,11 +172,15 @@ export function migrateCurrentToPreset(input) {
|
|
|
156
172
|
return null;
|
|
157
173
|
if (!Number.isFinite(input.contextWindow) || input.contextWindow <= 0)
|
|
158
174
|
return null;
|
|
175
|
+
const provider = input.provider ?? 'openai';
|
|
176
|
+
const anthropicPromptCache = provider === 'anthropic' && input.anthropicPromptCache !== false;
|
|
159
177
|
const existing = listPresets();
|
|
160
|
-
const dup = existing.find((p) => p.
|
|
178
|
+
const dup = existing.find((p) => p.provider === provider &&
|
|
179
|
+
p.baseURL === input.baseURL &&
|
|
161
180
|
p.apiKey === input.apiKey &&
|
|
162
181
|
p.model === input.model &&
|
|
163
|
-
p.contextWindow === input.contextWindow
|
|
182
|
+
p.contextWindow === input.contextWindow &&
|
|
183
|
+
p.anthropicPromptCache === anthropicPromptCache);
|
|
164
184
|
if (dup)
|
|
165
185
|
return null;
|
|
166
186
|
// 'default' 已被占 → 用户已显式起过预设,无需老数据迁入;返回 null 让调用方跳过即可。
|
|
@@ -168,10 +188,12 @@ export function migrateCurrentToPreset(input) {
|
|
|
168
188
|
return null;
|
|
169
189
|
savePreset({
|
|
170
190
|
name: 'default',
|
|
191
|
+
provider,
|
|
171
192
|
baseURL: input.baseURL,
|
|
172
193
|
apiKey: input.apiKey,
|
|
173
194
|
model: input.model,
|
|
174
195
|
contextWindow: input.contextWindow,
|
|
196
|
+
anthropicPromptCache,
|
|
175
197
|
});
|
|
176
198
|
return 'default';
|
|
177
199
|
}
|
|
@@ -131,6 +131,26 @@ export function recordArtifact(state, history, idx, output, succeeded) {
|
|
|
131
131
|
stateFor(state).artifacts.set(id, artifact);
|
|
132
132
|
updateStats(state, stateFor(state));
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* 按读取时间倒序返回最近若干「仍新鲜的 read_file 目标」(path + hash)。
|
|
136
|
+
* 用途:文件编辑工具参数校验失败(如缺 path)时,把系统已知的候选直接回灌给模型照抄,
|
|
137
|
+
* 避免模型在长上下文里凭记忆复述出错、补一个字段丢另一个字段的乒乓重试。
|
|
138
|
+
* 只展示事实、不替模型填值——选哪个候选仍由模型判断。永不抛错。
|
|
139
|
+
*/
|
|
140
|
+
export function knownEditTargets(state, limit = 3) {
|
|
141
|
+
const artifacts = stateFor(state).artifacts;
|
|
142
|
+
const targets = [];
|
|
143
|
+
for (const artifact of artifacts.values()) {
|
|
144
|
+
if (artifact.freshness !== 'fresh' || artifact.source.type !== 'read')
|
|
145
|
+
continue;
|
|
146
|
+
const dependency = artifact.dependencies[0];
|
|
147
|
+
if (!dependency || dependency.path === '*' || !dependency.hash)
|
|
148
|
+
continue;
|
|
149
|
+
targets.push({ path: dependency.path, hash: dependency.hash, version: artifact.version ?? 0 });
|
|
150
|
+
}
|
|
151
|
+
targets.sort((a, b) => b.version - a.version);
|
|
152
|
+
return targets.slice(0, Math.max(1, Math.floor(limit) || 3)).map(({ path, hash }) => ({ path, hash }));
|
|
153
|
+
}
|
|
134
154
|
function affected(artifact, changed) {
|
|
135
155
|
// '*' 依赖(无法解析出具体文件路径的诊断/搜索结果)不与任何具体写操作关联:
|
|
136
156
|
// 任何文件写入都会作废全部 '*' artifact,等于每次 mutation 都销毁
|
package/dist/context/budget.js
CHANGED
|
@@ -54,8 +54,10 @@ export function userTurnBoundary(history, window) {
|
|
|
54
54
|
/** 评估当前 history 的五区预算(纯函数,改不动 history)。
|
|
55
55
|
* 传入 step 是当前所在 step 编号(agent 循环 step 变量),用于日志/调试。
|
|
56
56
|
* correction:API 实测 / 估算的校正系数(默认 1);>1 表示粗估偏低,乘以系数后 actual 更接近真实值。
|
|
57
|
-
* activeTools 必须与下一次 chat() 实际发送的工具集合一致,避免 plan/子 agent 误算 schema。
|
|
58
|
-
|
|
57
|
+
* activeTools 必须与下一次 chat() 实际发送的工具集合一致,避免 plan/子 agent 误算 schema。
|
|
58
|
+
* ephemeralTokens:本次请求会追加、但不在 history 里的尾部注入(会话状态提醒等)的裸 token,
|
|
59
|
+
* 必须传入,否则压力线看不见这部分开销(见 SystemCostBreakdown.ephemeral)。 */
|
|
60
|
+
export function evaluateBudget(history, window, step = 0, correction = 1, activeTools = chatTools, ephemeralTokens = 0) {
|
|
59
61
|
const layers = {};
|
|
60
62
|
for (const k of BUDGET_LAYERS) {
|
|
61
63
|
const budget = Math.floor(BUDGET_RATIO[k] * window);
|
|
@@ -66,12 +68,17 @@ export function evaluateBudget(history, window, step = 0, correction = 1, active
|
|
|
66
68
|
// 裸总量(不乘 correction):硬闸用它判断,防止 correction 折扣否决真实溢出。
|
|
67
69
|
let rawTotal = 0;
|
|
68
70
|
const sysMsg = history[0];
|
|
71
|
+
const safeEphemeral = Number.isFinite(ephemeralTokens)
|
|
72
|
+
? Math.max(0, Math.round(ephemeralTokens))
|
|
73
|
+
: 0;
|
|
69
74
|
const systemCosts = {
|
|
70
75
|
prompt: sysMsg ? msgTokens(sysMsg) : 0,
|
|
71
76
|
toolSchemas: estimateToolSchemaTokens(activeTools),
|
|
77
|
+
ephemeralInjection: safeEphemeral,
|
|
72
78
|
};
|
|
73
|
-
// 工具 schema
|
|
74
|
-
|
|
79
|
+
// 工具 schema、system prompt 与尾部 ephemeral 注入同属请求固定开销;
|
|
80
|
+
// 必须计入总量才能可靠触发压缩(尾部注入不在 history 里,只能由调用方传入)。
|
|
81
|
+
const systemRaw = systemCosts.prompt + systemCosts.toolSchemas + systemCosts.ephemeralInjection;
|
|
75
82
|
layers.system.actual = adj(systemRaw);
|
|
76
83
|
rawTotal += systemRaw;
|
|
77
84
|
// Summary 检测:role:'system' 且不是 history[0] 的,视为摘要(compact.ts 摘要插 index 1)。
|
|
@@ -143,7 +150,7 @@ export function scheduleActions(report) {
|
|
|
143
150
|
const actions = [];
|
|
144
151
|
const { layers } = report;
|
|
145
152
|
if (layers.system.overBudget) {
|
|
146
|
-
const { prompt, toolSchemas } = report.systemCosts;
|
|
153
|
+
const { prompt, toolSchemas, ephemeralInjection } = report.systemCosts;
|
|
147
154
|
const { actual, budget } = layers.system;
|
|
148
155
|
const excess = actual - budget;
|
|
149
156
|
const percent = ((actual / Math.max(budget, 1)) * 100).toFixed(0);
|
|
@@ -151,7 +158,9 @@ export function scheduleActions(report) {
|
|
|
151
158
|
kind: 'warn',
|
|
152
159
|
layer: 'system',
|
|
153
160
|
reason: `固定开销 ${actual}/${budget} (+${excess}, ${percent}%);`
|
|
154
|
-
+ `提示 ${prompt} + 工具 ${toolSchemas}
|
|
161
|
+
+ `提示 ${prompt} + 工具 ${toolSchemas}`
|
|
162
|
+
+ (ephemeralInjection > 0 ? ` + 尾部注入 ${ephemeralInjection}` : '')
|
|
163
|
+
+ `,×${report.correction.toFixed(2)}。`,
|
|
155
164
|
});
|
|
156
165
|
}
|
|
157
166
|
const pressureLine = DEFAULT_BUDGET_POLICY.pressureTriggerRatio * report.window;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 顶部加 `# N entries · table-encoded` 计数头。
|
|
7
7
|
*
|
|
8
8
|
* 不变量(离线脚本断言):id/name/summary/type 全保留;仅 `active` 状态被省略(默认值,无信息损失)。
|
|
9
|
-
* 正则锚定整行 + 要求 `id: ... — ... (type, active)` 形,不匹配的行原样返回(防误伤
|
|
9
|
+
* 正则锚定整行 + 要求 `id: ... — ... (type, active)` 形,不匹配的行原样返回(防误伤 AGENTS.md 等正文)。
|
|
10
10
|
* group1 贪婪捕获到 `(\w+` 为止,故 name/summary 内含 ` — ` 或 ` (` 也不影响(仅丢尾部 `, active)`)。
|
|
11
11
|
*/
|
|
12
12
|
const ACTIVE_LINE_RE = /^(- [^:]+: .+ — .+ \(\w+), active\)$/;
|