mocode-ai 1.2.6 → 1.2.8
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 +72 -32
- 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 +66 -4
- package/dist/config/presets.js +29 -7
- package/dist/context/artifacts.js +20 -0
- package/dist/context/budget.js +20 -7
- package/dist/context/index.js +1 -1
- package/dist/host/stdio.js +10 -1
- package/dist/llm/index.js +31 -6
- package/dist/llm/providers/anthropic.js +370 -0
- package/dist/repl/index.js +85 -40
- package/dist/session/compact.js +121 -34
- package/dist/session/persist.js +2 -2
- package/dist/session/scheduler.js +4 -4
- 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/constants.js +14 -0
- package/dist/tools/registry.js +5 -1
- package/dist/ui/layout.js +11 -0
- package/dist/verification/prompt.js +55 -0
- package/package.json +2 -2
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// skill description 进化循环(自进化 Phase 1b)。
|
|
2
|
+
//
|
|
3
|
+
// 协议移植自 skill-creator 的 run_loop.py(调研见 docs/skill-self-evolution-research.md Part 1.2):
|
|
4
|
+
// - 分层切 train/holdout(默认 0.4)防过拟合
|
|
5
|
+
// - 每轮:LLM 读失败案例 → 提案新 description → 评测 → 接受门
|
|
6
|
+
// - 接受门(保守双条件):train 严格提升 且 holdout 不降;holdout 为空时退化为全集严格提升
|
|
7
|
+
// - 拒绝缓冲:被拒的候选记下来,后续提案明确告知「这些已试过且更差」,防重复打转
|
|
8
|
+
// - 迭代上限(默认 5);全程记录 history,返回最佳 description
|
|
9
|
+
//
|
|
10
|
+
// 安全边界(调研 Part 3):
|
|
11
|
+
// - 默认 dry-run(只打印不落盘);--apply 才写回 SKILL.md,且写前再显式确认一次
|
|
12
|
+
// - builtin skill 无磁盘载体,拒绝(入口在 skill-eval.loadSkillForEval)
|
|
13
|
+
// - 进化对象仅 description 一行;正文/脚本/执行面字段不在自动编辑范围
|
|
14
|
+
//
|
|
15
|
+
// 依赖:skill-eval.ts(评测引擎 + 纯函数)+ llm.chat(提案,单轮无工具)。
|
|
16
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { chat } from '../llm/index.js';
|
|
18
|
+
import { runTriggerEval, splitTriggerSet, applyDescription, MAX_DESCRIPTION_CHARS, } from './skill-eval.js';
|
|
19
|
+
/** 提案 LLM 的系统指令:确定性模板(不用 t(),保证跨语言可比、可单测断言)。 */
|
|
20
|
+
export function buildImprovePrompt(skill, currentDescription, failingCases, rejected) {
|
|
21
|
+
const body = (skill.body ?? '').slice(0, 1500);
|
|
22
|
+
const failLines = failingCases
|
|
23
|
+
.map((c) => {
|
|
24
|
+
const expect = c.should_trigger ? 'SHOULD trigger' : 'should NOT trigger';
|
|
25
|
+
return `- "${c.query}" → ${expect}, actual trigger rate ${c.trigger_rate.toFixed(2)}`;
|
|
26
|
+
})
|
|
27
|
+
.join('\n');
|
|
28
|
+
const rejectedBlock = rejected.length
|
|
29
|
+
? `\nThese descriptions were already tried and performed WORSE. Do not propose any of them or near-duplicates:\n${rejected
|
|
30
|
+
.map((d) => `- ${d}`)
|
|
31
|
+
.join('\n')}\n`
|
|
32
|
+
: '';
|
|
33
|
+
return [
|
|
34
|
+
`You are optimizing the frontmatter "description" field of an agent skill named "${skill.name}".`,
|
|
35
|
+
'The description is the ONLY text the model sees when deciding whether to trigger the skill.',
|
|
36
|
+
`The skill body (what it actually does) begins with:\n---\n${body}\n---`,
|
|
37
|
+
`Current description:\n${currentDescription}`,
|
|
38
|
+
'Test results with the current description (failures to fix):',
|
|
39
|
+
failLines,
|
|
40
|
+
rejectedBlock,
|
|
41
|
+
'Propose ONE improved description that makes the skill trigger for the SHOULD-trigger cases and stay silent for the should-NOT-trigger cases.',
|
|
42
|
+
'Rules:',
|
|
43
|
+
'- Output ONLY the description text, nothing else (no quotes, no "description:", no markdown).',
|
|
44
|
+
'- One line only, 100-400 characters.',
|
|
45
|
+
'- Write it as a routing rule: state WHAT it does and WHEN to use it (concrete triggers/phrases), not as documentation.',
|
|
46
|
+
'- Preserve the language of the current description (English stays English, Chinese stays Chinese).',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 从 LLM 输出里提取候选 description(纯函数):
|
|
51
|
+
* 去首尾空白 → 若以 "description:" 开头则剥掉 → 只取第一行 → 去配对引号 → 校验长度。
|
|
52
|
+
* 非法(空 / 超长)返 null。
|
|
53
|
+
*/
|
|
54
|
+
export function extractDescriptionProposal(text, maxChars = MAX_DESCRIPTION_CHARS) {
|
|
55
|
+
let t = (text ?? '').trim();
|
|
56
|
+
if (!t)
|
|
57
|
+
return null;
|
|
58
|
+
t = t.replace(/^description\s*[::]\s*/i, '');
|
|
59
|
+
let first = t.split(/\r?\n/)[0].trim();
|
|
60
|
+
if (!first)
|
|
61
|
+
return null;
|
|
62
|
+
if (first.length >= 2 && (first[0] === '"' || first[0] === "'") && first[first.length - 1] === first[0]) {
|
|
63
|
+
first = first.slice(1, -1).trim();
|
|
64
|
+
}
|
|
65
|
+
if (!first || first.length > maxChars)
|
|
66
|
+
return null;
|
|
67
|
+
return first;
|
|
68
|
+
}
|
|
69
|
+
/** 接受门(纯函数):train 严格提升 且 holdout 不降;holdout 为空时全集严格提升。 */
|
|
70
|
+
export function shouldAcceptCandidate(base, cand) {
|
|
71
|
+
const trainUp = cand.trainPass > base.trainPass;
|
|
72
|
+
if (!trainUp) {
|
|
73
|
+
return { accept: false, reason: `train 未提升 (${cand.trainPass}/${cand.trainTotal} ≤ ${base.trainPass}/${base.trainTotal})` };
|
|
74
|
+
}
|
|
75
|
+
if (base.holdoutPass === null || cand.holdoutPass === null) {
|
|
76
|
+
return { accept: true, reason: `train 提升且无 holdout(${base.trainPass}→${cand.trainPass})` };
|
|
77
|
+
}
|
|
78
|
+
if (cand.holdoutPass < base.holdoutPass) {
|
|
79
|
+
return { accept: false, reason: `holdout 回退 (${cand.holdoutPass} < ${base.holdoutPass}),过拟合 train` };
|
|
80
|
+
}
|
|
81
|
+
return { accept: true, reason: `train ${base.trainPass}→${cand.trainPass},holdout ${base.holdoutPass}→${cand.holdoutPass}(不降)` };
|
|
82
|
+
}
|
|
83
|
+
/** 把报告按 query 集合拆成 train / holdout 的分项(纯函数)。 */
|
|
84
|
+
function partitionReport(report, trainQueries) {
|
|
85
|
+
const train = report.results.filter((r) => trainQueries.has(r.query));
|
|
86
|
+
const holdout = report.results.filter((r) => !trainQueries.has(r.query));
|
|
87
|
+
return {
|
|
88
|
+
trainPass: train.filter((r) => r.pass).length,
|
|
89
|
+
trainTotal: train.length,
|
|
90
|
+
holdoutPass: holdout.length ? holdout.filter((r) => r.pass).length : null,
|
|
91
|
+
holdoutTotal: holdout.length,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function failingCases(report) {
|
|
95
|
+
return report.results
|
|
96
|
+
.filter((r) => !r.pass)
|
|
97
|
+
.map((r) => ({ query: r.query, should_trigger: r.should_trigger, trigger_rate: r.trigger_rate }));
|
|
98
|
+
}
|
|
99
|
+
/** 调 LLM 提案一次(单轮、无工具、超时 abort)。失败抛错由调用方捕获。 */
|
|
100
|
+
async function proposeDescription(prompt, timeoutMs, signal) {
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
103
|
+
// AbortSignal.any 需 Node ≥20;engines>=18,手动桥接外部 signal。
|
|
104
|
+
const externalAbort = () => controller.abort();
|
|
105
|
+
if (signal) {
|
|
106
|
+
if (signal.aborted)
|
|
107
|
+
controller.abort();
|
|
108
|
+
else
|
|
109
|
+
signal.addEventListener('abort', externalAbort, { once: true });
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const messages = [{ role: 'user', content: prompt }];
|
|
113
|
+
const res = await chat(messages, {}, controller.signal);
|
|
114
|
+
return extractDescriptionProposal(res.content ?? '');
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
signal?.removeEventListener('abort', externalAbort);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 进化主循环。evalSet 必须非空(调用方保证;无 evals 的 skill 在 CLI 层拒绝——
|
|
123
|
+
* 「没验证门的 skill 禁止自动落盘」,调研共识 1)。
|
|
124
|
+
*/
|
|
125
|
+
export async function runImproveLoop(skill, evalSet, opts = {}) {
|
|
126
|
+
const maxIterations = opts.maxIterations ?? 5;
|
|
127
|
+
const runsPerQuery = opts.runsPerQuery ?? 3;
|
|
128
|
+
const threshold = opts.threshold ?? 0.5;
|
|
129
|
+
const { train, holdout } = splitTriggerSet(evalSet, opts.holdout ?? 0.4, opts.seed ?? 42);
|
|
130
|
+
if (train.length === 0)
|
|
131
|
+
throw new Error('train 集为空:eval 集太小,无法进化(至少需要 2 条且覆盖触发/不触发)');
|
|
132
|
+
const trainQueries = new Set(train.map((t) => t.query));
|
|
133
|
+
const evalOpts = { runsPerQuery, threshold, timeoutMs: opts.timeoutMs, signal: opts.signal };
|
|
134
|
+
let baseDescription = skill.description;
|
|
135
|
+
let baseReport = await runTriggerEval(skill, baseDescription, evalSet, runsPerQuery, threshold, evalOpts);
|
|
136
|
+
let baseParts = partitionReport(baseReport, trainQueries);
|
|
137
|
+
const iterations = [];
|
|
138
|
+
const rejected = [];
|
|
139
|
+
let bestDescription = baseDescription;
|
|
140
|
+
let bestKey = [baseParts.trainPass, baseParts.holdoutPass ?? 0];
|
|
141
|
+
const earlyExit = () => baseParts.trainTotal > 0 &&
|
|
142
|
+
baseParts.trainPass === baseParts.trainTotal &&
|
|
143
|
+
(baseParts.holdoutPass === null || (baseParts.holdoutTotal > 0 && baseParts.holdoutPass === baseParts.holdoutTotal));
|
|
144
|
+
if (earlyExit()) {
|
|
145
|
+
opts.onIteration?.(0, maxIterations, '当前 description 在 train + holdout 全通过,无需进化。');
|
|
146
|
+
}
|
|
147
|
+
for (let iter = 1; iter <= maxIterations && !earlyExit(); iter++) {
|
|
148
|
+
opts.onIteration?.(iter, maxIterations, `第 ${iter}/${maxIterations} 轮:提案新 description…`);
|
|
149
|
+
const prompt = buildImprovePrompt(skill, baseDescription, failingCases(baseReport), rejected);
|
|
150
|
+
let candidate;
|
|
151
|
+
try {
|
|
152
|
+
candidate = await proposeDescription(prompt, opts.proposeTimeoutMs ?? 120_000, opts.signal);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
156
|
+
opts.onIteration?.(iter, maxIterations, `提案失败(${msg.slice(0, 120)}),终止。`);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
if (!candidate || candidate === baseDescription) {
|
|
160
|
+
opts.onIteration?.(iter, maxIterations, '提案无效(空/超长/与当前相同),终止。');
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
const candReport = await runTriggerEval(skill, candidate, evalSet, runsPerQuery, threshold, evalOpts);
|
|
164
|
+
const candParts = partitionReport(candReport, trainQueries);
|
|
165
|
+
const gate = shouldAcceptCandidate(baseParts, candParts);
|
|
166
|
+
const iteration = {
|
|
167
|
+
iteration: iter,
|
|
168
|
+
description: candidate,
|
|
169
|
+
trainPass: candParts.trainPass,
|
|
170
|
+
trainTotal: candParts.trainTotal,
|
|
171
|
+
holdoutPass: candParts.holdoutPass,
|
|
172
|
+
holdoutTotal: candParts.holdoutTotal,
|
|
173
|
+
accepted: gate.accept,
|
|
174
|
+
reason: gate.reason,
|
|
175
|
+
};
|
|
176
|
+
iterations.push(iteration);
|
|
177
|
+
opts.onIteration?.(iter, maxIterations, `${gate.accept ? '✓ 接受' : '· 拒绝'}: train ${candParts.trainPass}/${candParts.trainTotal}, holdout ${candParts.holdoutPass ?? '-'}/${candParts.holdoutTotal} — ${gate.reason}`);
|
|
178
|
+
if (gate.accept) {
|
|
179
|
+
baseDescription = candidate;
|
|
180
|
+
baseReport = candReport;
|
|
181
|
+
baseParts = candParts;
|
|
182
|
+
const key = [candParts.trainPass, candParts.holdoutPass ?? 0];
|
|
183
|
+
if (key[0] > bestKey[0] || (key[0] === bestKey[0] && key[1] > bestKey[1])) {
|
|
184
|
+
bestKey = key;
|
|
185
|
+
bestDescription = candidate;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
rejected.push(candidate);
|
|
190
|
+
if (rejected.length > 5)
|
|
191
|
+
rejected.shift();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
skill: skill.name,
|
|
196
|
+
originalDescription: skill.description,
|
|
197
|
+
bestDescription,
|
|
198
|
+
improved: bestDescription !== skill.description,
|
|
199
|
+
iterations,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 把进化结果写回 SKILL.md 的 description(唯一的落盘点;CLI --apply 时才调)。
|
|
204
|
+
* 返回写后的文件内容;frontmatter 无法安全替换时抛错(不落盘)。
|
|
205
|
+
*/
|
|
206
|
+
export function applyImprovedDescription(skill, description) {
|
|
207
|
+
if (skill.dir === 'builtin')
|
|
208
|
+
throw new Error('内置 skill 不可修改');
|
|
209
|
+
if (description.length > MAX_DESCRIPTION_CHARS)
|
|
210
|
+
throw new Error(`description 超长(>${MAX_DESCRIPTION_CHARS})`);
|
|
211
|
+
const original = readFileSync(skill.skillMdPath, 'utf8');
|
|
212
|
+
const next = applyDescription(original, description);
|
|
213
|
+
if (next === null) {
|
|
214
|
+
throw new Error('无法定位 SKILL.md frontmatter 的 description 字段,拒绝写回(请手工编辑)');
|
|
215
|
+
}
|
|
216
|
+
if (next === original) {
|
|
217
|
+
throw new Error('写回无变化(description 与当前一致)');
|
|
218
|
+
}
|
|
219
|
+
writeFileSync(skill.skillMdPath, next, 'utf8');
|
|
220
|
+
return next;
|
|
221
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// skill 使用台账(自进化 Phase 0)。
|
|
2
|
+
// 为什么不在 trace.jsonl 上做:工具事件出于隐私只存参数指纹(sha256/keys,
|
|
3
|
+
// trace-sanitize.ts 刻意不存值),拿不到 skill name。台账因此在工具层直接记录——
|
|
4
|
+
// use_skill / run_skill 是唯一知道真实 skill name 的落点。
|
|
5
|
+
//
|
|
6
|
+
// 落盘:<cwd>/.mocode/skill-stats.jsonl(append-only JSONL,每行一次使用)。
|
|
7
|
+
// 纯观测:任何写失败静默吞掉,绝不阻断 agent 主流程(风格对齐 session/trace.ts)。
|
|
8
|
+
// 聚合是纯函数(aggregateSkillStats),吃记录数组吐按 skill 的计数,便于单测。
|
|
9
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
/**
|
|
12
|
+
* 台账路径(项目级,与 sessions 同根;使用是项目上下文相关的,不写全局)。
|
|
13
|
+
* baseDir 可选:测试指向临时目录用;缺省 process.cwd()。
|
|
14
|
+
*/
|
|
15
|
+
export function skillStatsPath(baseDir = process.cwd()) {
|
|
16
|
+
return path.join(baseDir, '.mocode', 'skill-stats.jsonl');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 追加一条台账记录;任何失败静默(观测不得阻断主流程)。
|
|
20
|
+
* MOCODE_SKILL_EVAL=1(触发评测进程内设置)时跳过:评测里的人工构造调用
|
|
21
|
+
* 是测量手段不是真实使用,记入会污染自进化的输入信号。
|
|
22
|
+
*/
|
|
23
|
+
export function recordSkillUsage(rec, baseDir = process.cwd()) {
|
|
24
|
+
if (process.env.MOCODE_SKILL_EVAL === '1')
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
const p = skillStatsPath(baseDir);
|
|
28
|
+
const dir = path.dirname(p);
|
|
29
|
+
if (!existsSync(dir))
|
|
30
|
+
mkdirSync(dir, { recursive: true });
|
|
31
|
+
appendFileSync(p, JSON.stringify(rec) + '\n', 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// 观测失败静默
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** 读取台账原始记录;文件不存在 / 单行损坏 → 跳过(不抛)。 */
|
|
38
|
+
export function loadSkillUsage(baseDir = process.cwd()) {
|
|
39
|
+
try {
|
|
40
|
+
const content = readFileSync(skillStatsPath(baseDir), 'utf8');
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const line of content.split('\n')) {
|
|
43
|
+
const s = line.trim();
|
|
44
|
+
if (!s)
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
const v = JSON.parse(s);
|
|
48
|
+
if (v && typeof v.skill === 'string' && (v.kind === 'use' || v.kind === 'run')) {
|
|
49
|
+
out.push(v);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// 损坏行跳过
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 纯聚合:记录 → 按 skill 的计数视图。按 skill 名分组(大小写敏感),
|
|
64
|
+
* lastUsedAt 取 ts 字符串字典序最大(ISO 时间戳字典序 == 时间序)。
|
|
65
|
+
* 输入乱序也安全。空输入返回 []。
|
|
66
|
+
*/
|
|
67
|
+
export function aggregateSkillStats(records) {
|
|
68
|
+
const bySkill = new Map();
|
|
69
|
+
for (const r of records) {
|
|
70
|
+
const list = bySkill.get(r.skill);
|
|
71
|
+
if (list)
|
|
72
|
+
list.push(r);
|
|
73
|
+
else
|
|
74
|
+
bySkill.set(r.skill, [r]);
|
|
75
|
+
}
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const [skill, list] of bySkill) {
|
|
78
|
+
const runs = list.filter((r) => r.kind === 'run');
|
|
79
|
+
const runSuccess = runs.filter((r) => r.status === 'success').length;
|
|
80
|
+
let lastFailure = null;
|
|
81
|
+
let lastUsedAt = '';
|
|
82
|
+
for (const r of list) {
|
|
83
|
+
if (typeof r.ts === 'string' && r.ts > lastUsedAt)
|
|
84
|
+
lastUsedAt = r.ts;
|
|
85
|
+
if (r.status !== 'success' && (!lastFailure || r.ts > lastFailure.ts)) {
|
|
86
|
+
lastFailure = { ts: r.ts, status: r.status, code: r.code };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
out.push({
|
|
90
|
+
skill,
|
|
91
|
+
total: list.length,
|
|
92
|
+
uses: list.length - runs.length,
|
|
93
|
+
runs: runs.length,
|
|
94
|
+
runSuccessRate: runs.length ? runSuccess / runs.length : null,
|
|
95
|
+
lastFailure,
|
|
96
|
+
lastUsedAt,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// 按最近使用倒序,让 /skills 徽标与人工浏览都「最活跃的在前」。
|
|
100
|
+
out.sort((a, b) => (a.lastUsedAt < b.lastUsedAt ? 1 : a.lastUsedAt > b.lastUsedAt ? -1 : 0));
|
|
101
|
+
return out;
|
|
102
|
+
}
|
package/dist/tools/constants.js
CHANGED
|
@@ -10,6 +10,20 @@ 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;
|
|
13
27
|
// ── 记忆(Tier-2 JSONL 工具库)──────────────────────────────────────────────
|
|
14
28
|
/** 单条记忆 body 上限(字符)。进 history 前 memory_search 结果另有 MAX_MEMORY_RESULT 兜底。 */
|
|
15
29
|
export const MAX_MEMORY_ENTRY = 4000;
|
package/dist/tools/registry.js
CHANGED
|
@@ -220,7 +220,11 @@ export async function executeToolOutcome(name, argsRaw, signal, opts) {
|
|
|
220
220
|
}
|
|
221
221
|
const validation = validateToolArguments(tool, parsed);
|
|
222
222
|
if (!validation.valid) {
|
|
223
|
-
|
|
223
|
+
const hint = opts?.argumentErrorHint?.trim();
|
|
224
|
+
const message = hint
|
|
225
|
+
? `错误:工具 ${name} 参数无效: ${validation.message}\n${hint}`
|
|
226
|
+
: `错误:工具 ${name} 参数无效: ${validation.message}`;
|
|
227
|
+
return terminalOutcome('error', validation.code, message, startedAt);
|
|
224
228
|
}
|
|
225
229
|
const args = parsed;
|
|
226
230
|
const sandboxError = enforceSandbox(name, args);
|
package/dist/ui/layout.js
CHANGED
|
@@ -671,6 +671,17 @@ export function contentDeleteFrom(startIdx, n) {
|
|
|
671
671
|
export function totalRows() {
|
|
672
672
|
return content.totalRows();
|
|
673
673
|
}
|
|
674
|
+
/** 缓冲尾部(已提交行)是否已经是空白行(去掉 ANSI 后无可见字符)。
|
|
675
|
+
* 供 compact 等在 step 循环顶部写通知行前判断是否需要补空行分隔。 */
|
|
676
|
+
export function isLastContentRowBlank() {
|
|
677
|
+
const committed = content.committedRows();
|
|
678
|
+
if (committed === 0)
|
|
679
|
+
return false;
|
|
680
|
+
const line = content.lineAt(committed - 1);
|
|
681
|
+
if (line === null)
|
|
682
|
+
return false;
|
|
683
|
+
return line.replace(/\x1b\[[0-9;]*m/g, '').trim().length === 0;
|
|
684
|
+
}
|
|
674
685
|
/** 正文→mutation 首摘要前,把尾部间距强制归一为一条视觉空行。 */
|
|
675
686
|
export function normalizeMutationBoundary() {
|
|
676
687
|
if (!active || !ui.isTTY)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { discoverPackageValidationCommands } from './discovery.js';
|
|
3
|
+
import { discoverProjectProfile } from './profile.js';
|
|
4
|
+
/** Keep the prompt section small on large monorepos; the agent can still discover the rest. */
|
|
5
|
+
const MAX_LISTED_PACKAGES = 8;
|
|
6
|
+
function displayRoot(profile, packageProfile) {
|
|
7
|
+
const relative = path.relative(profile.root, packageProfile.root);
|
|
8
|
+
return relative === '' ? '.' : relative.split(path.sep).join('/');
|
|
9
|
+
}
|
|
10
|
+
function lineFor(profile, packageProfile) {
|
|
11
|
+
const commands = discoverPackageValidationCommands(profile, packageProfile);
|
|
12
|
+
if (commands.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
const cwd = displayRoot(profile, packageProfile);
|
|
15
|
+
const rendered = commands.map((item) => `\`${item.command}\``).join(', ');
|
|
16
|
+
return `- ${packageProfile.name} (cwd \`${cwd}\`): ${rendered}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Deterministic project validation map injected into the system prompt: which package owns which
|
|
20
|
+
* script, and the exact command plus cwd to run it. Commands are listed in increasing cost order
|
|
21
|
+
* (typecheck → build → test) and are never executed here — this is evidence, not a completion gate.
|
|
22
|
+
*
|
|
23
|
+
* Returns '' when no package exposes a validation script, or when discovery fails for any reason
|
|
24
|
+
* (missing/invalid manifest, unreadable workspace): prompt construction must never break.
|
|
25
|
+
*/
|
|
26
|
+
export function buildValidationCommandsSection(root = process.cwd()) {
|
|
27
|
+
try {
|
|
28
|
+
const profile = discoverProjectProfile(root);
|
|
29
|
+
const lines = [];
|
|
30
|
+
let omitted = 0;
|
|
31
|
+
for (const packageProfile of profile.packages) {
|
|
32
|
+
const line = lineFor(profile, packageProfile);
|
|
33
|
+
if (!line)
|
|
34
|
+
continue;
|
|
35
|
+
if (lines.length >= MAX_LISTED_PACKAGES)
|
|
36
|
+
omitted += 1;
|
|
37
|
+
else
|
|
38
|
+
lines.push(line);
|
|
39
|
+
}
|
|
40
|
+
if (lines.length === 0)
|
|
41
|
+
return '';
|
|
42
|
+
if (omitted > 0) {
|
|
43
|
+
lines.push(`- …${omitted} more package(s) with scripts: read their package.json when needed.`);
|
|
44
|
+
}
|
|
45
|
+
return [
|
|
46
|
+
'',
|
|
47
|
+
'## Validation commands (discovered from project manifests)',
|
|
48
|
+
'Listed in increasing cost order. Use them when a check is worth running; prefer the package that owns your change over repository-wide runs. Not a completion gate.',
|
|
49
|
+
...lines,
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return ''; // Discovery is best-effort: never let it break prompt construction.
|
|
54
|
+
}
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"build": "tsc -p tsconfig.build.json",
|
|
24
24
|
"test": "tsc -p tsconfig.test-build.json && node --test --experimental-test-isolation=none \"dist-tests/tests/*.test.js\"",
|
|
25
25
|
"typecheck": "tsc --noEmit && tsc -p tests/tsconfig.json && tsc -p evals/tsconfig.json",
|
|
26
|
-
"eval:smoke": "tsx evals/smoke.ts && tsx evals/coding/smoke.ts",
|
|
26
|
+
"eval:smoke": "tsx evals/smoke.ts && tsx evals/coding/smoke.ts && tsx evals/work-discipline.ts",
|
|
27
27
|
"eval:coding": "tsx evals/coding/runner.ts",
|
|
28
28
|
"eval:coding:list": "tsx evals/coding/runner.ts --list",
|
|
29
29
|
"prepare": "npm run build"
|