mocode-ai 1.0.2 → 1.0.4
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 +13 -0
- package/README.zh-CN.md +13 -0
- package/dist/agent/core.js +132 -2
- package/dist/agent/index.js +8 -6
- package/dist/agent/middleware/checklist.js +59 -0
- package/dist/agent/retry-classifier.js +64 -0
- package/dist/agent/work-discipline.js +108 -0
- package/dist/config/index.js +5 -2
- package/dist/foo.js +28 -0
- package/dist/foo.test.js +12 -0
- package/dist/i18n/index.js +4 -0
- package/dist/repl/index.js +1 -1
- package/dist/session/scheduler.js +1 -1
- package/dist/session/trace-metrics.js +86 -1
- package/dist/tools/builtins/ask-human.js +41 -24
- package/dist/tools/retry.js +34 -2
- package/dist/tools/validation.js +11 -1
- package/dist/ui/batch.js +11 -8
- package/dist/ui/layout.js +10 -10
- package/dist/ui/render.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -279,6 +279,19 @@ Control via environment variables:
|
|
|
279
279
|
|
|
280
280
|
See [docs/USAGE_SNAPSHOT_SKILL.md](./docs/USAGE_SNAPSHOT_SKILL.md) for detailed usage.
|
|
281
281
|
|
|
282
|
+
## Working discipline (Build-and-Self-Verify)
|
|
283
|
+
|
|
284
|
+
Every coding task runs through four sequential phases. Skipping or merging them is a failure mode — the system prompt injects this discipline on every turn (see `src/agent/work-discipline.ts`):
|
|
285
|
+
|
|
286
|
+
1. **Plan & Discover** — restate the goal, identify the acceptance signal, read the relevant code, and surface ambiguities via `ask_human` before implementing.
|
|
287
|
+
2. **Build** — make the smallest change that satisfies the spec; tests for new/changed behavior are an obligation, not a "should" aspiration.
|
|
288
|
+
3. **Verify** — run a real, executable verification (typecheck, the project's test command, or a focused reproducer). Read the full output. Compare the result to the **spec**, not to your own diff.
|
|
289
|
+
4. **Fix** — any failure → return to the spec, not to the diff. Re-derive what the spec requires; after a fix, re-run Phase 3 end-to-end. Cap blind retries at three identical failed attempts before changing approach.
|
|
290
|
+
|
|
291
|
+
Hard rule: *"I read the code and it looks right" is not a completion signal.* A task is complete only when executable verification against the spec has run, its full output has been read, and the result matches the spec — and the final reply names the command, the output, and the spec line it satisfied.
|
|
292
|
+
|
|
293
|
+
The section adapts lightly per `model_family` (anthropic / openai / qwen) so the wording matches each base model's instruction-following style. All four variants share the same 4-phase English body; only the opener sentence and the `[model: X]` tag differ. User language preference is handled by the existing i18n block.
|
|
294
|
+
|
|
282
295
|
## Project memory (MOCODE.md)
|
|
283
296
|
|
|
284
297
|
MoCode has a **two-tier memory** model distinct from skills:
|
package/README.zh-CN.md
CHANGED
|
@@ -260,6 +260,19 @@ mocode 自动扫描以下目录的 skill(每个 skill 是 `<name>/SKILL.md`,带
|
|
|
260
260
|
|
|
261
261
|
skill 的 `description` 注入系统提示(渐进式披露第①层),模型只在任务相关时调 `use_skill` 加载完整正文(第②层)。用 `/skills` 查看已发现的 skill。
|
|
262
262
|
|
|
263
|
+
## 工作纪律(4 阶段 — Build-and-Self-Verify)
|
|
264
|
+
|
|
265
|
+
每个 coding 任务必须按顺序走完 4 个阶段。跳过/合并 = 失败模式 —— system prompt 每轮注入这段纪律(见 `src/agent/work-discipline.ts`):
|
|
266
|
+
|
|
267
|
+
1. **Plan & Discover** — 用一句话复述目标,明确验收信号(测试名/命令输出/文件存在/行为变化);写代码前先读相关代码;不可逆选择(删除/公开 API/权限)用 `ask_human` 主动澄清。
|
|
268
|
+
2. **Build** — 用最小改动满足 spec,不夹带无关重构;新/改行为必须有对应测试 —— 目标里的 "should" 是义务。
|
|
269
|
+
3. **Verify** — 跑**真实**可执行验证(typecheck/项目 test 命令/聚焦 reproducer),读完整输出,与 **spec** 对比,不是与自己的 diff 对比。
|
|
270
|
+
4. **Fix** — 任何失败 → 回 spec,不是回 diff;修完重跑 Phase 3 全程;同工具同参数 3 次连续失败后**换思路**(换工具/换不变量/`ask_human`)。
|
|
271
|
+
|
|
272
|
+
**硬规则:** "我读代码觉得对"不是完成信号。任务完成的唯一判据:对 spec 的可执行验证已跑过、完整输出已读、结果与 spec 匹配 —— 最终回复里**显式给出证据**(哪个命令、哪段输出、对应 spec 哪一行)。
|
|
273
|
+
|
|
274
|
+
段内措辞按 `model_family`(anthropic / openai / qwen)轻量适配,贴合各 base model 的指令遵从习惯。4 份共用同一套 4 阶段结构 + 英文纪律文本,只在首句与 `[model: X]` 标签上区分;用户语言偏好由现有 i18n 段负责。
|
|
275
|
+
|
|
263
276
|
## 项目记忆(MOCODE.md)
|
|
264
277
|
|
|
265
278
|
mocode 的**双层记忆**模型,跟 Skills 是两件事:
|
package/dist/agent/core.js
CHANGED
|
@@ -10,6 +10,9 @@ import { executeToolOutcome, getToolCapabilities, isFileMutationTool, tools, } f
|
|
|
10
10
|
import { checkPermission } from '../permissions/index.js';
|
|
11
11
|
import { validateToolArguments } from '../tools/validation.js';
|
|
12
12
|
import { getPlanDisabledTools, getRuntimeDisabledTools } from '../tools/constants.js';
|
|
13
|
+
import { createPreCompletionChecklistMiddleware, } from './middleware/checklist.js';
|
|
14
|
+
import { inferModelFamily } from './work-discipline.js';
|
|
15
|
+
import { reflectionHint, classifyError } from './retry-classifier.js';
|
|
13
16
|
import { getAgentMode, setAgentMode } from './mode.js';
|
|
14
17
|
import { maybeCompact, contextState, dropContextFromHistory, createTraceEvent, summarizeToolArguments, safeProviderId, } from '../session/index.js';
|
|
15
18
|
import { createBudgetScheduler } from '../session/scheduler.js';
|
|
@@ -165,6 +168,39 @@ function readDiffContext(tc, parsed) {
|
|
|
165
168
|
* - lifecycle 也在每个 runAgentCore 实例化一次,登记 grep/glob/codegraph 等 producer
|
|
166
169
|
* 与 read/edit/write 的 consumer 关系;孤立+老化自动 STUB(观察类工具永不到 STUB)。
|
|
167
170
|
* - 开关关闭时 lifecycle=null 完全跳过。 */
|
|
171
|
+
/** RETRY-01: 把 thrash hint + 反思指针一次性拼到 output 尾部。
|
|
172
|
+
* - thrash hint 永远可能存在(连续失败才出现);
|
|
173
|
+
* - 反思指针仅在 error/denied 状态追加,success 跳过避免噪声。
|
|
174
|
+
* 抽出 helper 是为了 sequential + parallel 两处共用,避免拼装逻辑漂移。
|
|
175
|
+
* 返回追加后的字符串 + 反思 category(用于 QUAL-01 trace 硬事件),
|
|
176
|
+
* success / aborted 时 category === null,调用方据此决定是否 emit 'retry_reflection' 事件。 */
|
|
177
|
+
function appendRetryAnnotations(output, status, code, thrashHint) {
|
|
178
|
+
let result = thrashHint ? `${output}${thrashHint}` : output;
|
|
179
|
+
let reflectionCategory = null;
|
|
180
|
+
if (status !== 'success' && status !== 'aborted') {
|
|
181
|
+
reflectionCategory = classifyError(code);
|
|
182
|
+
const reflection = reflectionHint(reflectionCategory);
|
|
183
|
+
result = `${result}\n\n[retry reflection: ${reflectionCategory}]\n${reflection}`;
|
|
184
|
+
}
|
|
185
|
+
return { output: result, reflectionCategory };
|
|
186
|
+
}
|
|
187
|
+
/** ASK-01: ask_human 工具调用本 turn 计数器。纯函数无副作用,
|
|
188
|
+
* 计数规则简单:1-based(本 turn 第 1 次 = 1, 第 2 次 = 2, 第 3+ 次 = exceeded)。
|
|
189
|
+
* 预算上限常量与工作纪律段 "Budget: at most 2 ask_human calls per turn" 保持一致;
|
|
190
|
+
* 改这里时,fixture 与纪律段都要同步。 */
|
|
191
|
+
export const ASK_HUMAN_PER_TURN_BUDGET = 2;
|
|
192
|
+
export function askHumanBudgetAnnotation(askHumanCountThisTurn, status) {
|
|
193
|
+
// 工具失败 / aborted 不追加(避免噪声;模型已被错误消息告知失败)。
|
|
194
|
+
if (status !== 'success')
|
|
195
|
+
return null;
|
|
196
|
+
if (askHumanCountThisTurn < ASK_HUMAN_PER_TURN_BUDGET)
|
|
197
|
+
return null;
|
|
198
|
+
if (askHumanCountThisTurn === ASK_HUMAN_PER_TURN_BUDGET) {
|
|
199
|
+
return `\n\n[ask budget] This was your ${askHumanCountThisTurn}nd ask_human call this turn (budget = ${ASK_HUMAN_PER_TURN_BUDGET}). For the rest of this turn, prefer the safer default and disclose the choice in your final reply — do not silently guess.`;
|
|
200
|
+
}
|
|
201
|
+
// 第 3+ 次(超过预算):强硬提示,鼓励模型停下问自己是否还有意义。
|
|
202
|
+
return `\n\n[ask budget EXCEEDED] This is ask_human call #${askHumanCountThisTurn} this turn (budget = ${ASK_HUMAN_PER_TURN_BUDGET}). Stop asking; choose a default, implement, and disclose the choice in your final reply. Continuing to ask is more harmful than a documented guess.`;
|
|
203
|
+
}
|
|
168
204
|
function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runtimeContextState = contextState, succeededOverride) {
|
|
169
205
|
const succeeded = succeededOverride ?? isToolResultSuccess(output);
|
|
170
206
|
const ageAware = config.contextOptimize ? ageAwareStateFor(history) : null;
|
|
@@ -203,6 +239,16 @@ function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runt
|
|
|
203
239
|
export async function runAgentCore(opts) {
|
|
204
240
|
const { history, userInput, signal, onContextUpdate, hooks } = opts;
|
|
205
241
|
const runtimeContextState = opts.contextState ?? contextState;
|
|
242
|
+
// ASK-01: 本 turn 已使用的 ask_human 次数(每次 tool call 后递增)。
|
|
243
|
+
// 预算超过时,工具结果尾部追加 ask budget 提示,不直接拒绝调用(更轻量,
|
|
244
|
+
// 也避免和现有 permission 系统的"拒绝"语义重叠)。
|
|
245
|
+
let askHumanCountThisTurn = 0;
|
|
246
|
+
// PROMPT-02: 解析 preCompletionChecklist 选项。undefined = 启用默认 middleware;
|
|
247
|
+
// false = opt-out(checklist 自身调试用);function = 调用方自定义。
|
|
248
|
+
const _checklistMiddleware = createPreCompletionChecklistMiddleware();
|
|
249
|
+
const preCompletionChecklistHandler = opts.preCompletionChecklist === false
|
|
250
|
+
? null
|
|
251
|
+
: (opts.preCompletionChecklist ?? _checklistMiddleware.handler);
|
|
206
252
|
const maxSteps = opts.maxSteps ?? config.maxSteps;
|
|
207
253
|
// 中断还原:LLM 中途可能调 switch_mode 切了模式,abort 时连同模式一起还原回轮首。
|
|
208
254
|
const savedMode = getAgentMode();
|
|
@@ -598,7 +644,30 @@ export async function runAgentCore(opts) {
|
|
|
598
644
|
hooks.onToolResult?.(tc, output, null, null, 1); // 并行工具无 diff
|
|
599
645
|
// Thrashing:history 里附 hint(UI 已用干净 output 渲染,避免屏幕噪声)
|
|
600
646
|
const hint = recordAndHint(tc.name, tc.arguments, outcome.status === 'success');
|
|
601
|
-
|
|
647
|
+
const { output: annotated, reflectionCategory } = appendRetryAnnotations(output, outcome.status, outcome.code, hint);
|
|
648
|
+
// QUAL-01 trace 硬事件:反思指针注入计数(history 文本扫描会因 LLM 在
|
|
649
|
+
// 正文里提一句 "[retry reflection]" 而误算,改用 core 接缝处显式 emit)。
|
|
650
|
+
if (reflectionCategory !== null) {
|
|
651
|
+
emitTrace('retry_reflection', {
|
|
652
|
+
tool: tc.name,
|
|
653
|
+
code: outcome.code,
|
|
654
|
+
category: reflectionCategory,
|
|
655
|
+
attempt: outcome.attempts ?? 1,
|
|
656
|
+
}, tc.id ? { providerToolCallId: tc.id } : {});
|
|
657
|
+
}
|
|
658
|
+
// ASK-01: 计数 + 预算提示
|
|
659
|
+
if (tc.name === 'ask_human' && outcome.status === 'success') {
|
|
660
|
+
askHumanCountThisTurn += 1;
|
|
661
|
+
// QUAL-01 trace 硬事件:ask_human 真实调用计数(取代 tool_call_end.name 推断)。
|
|
662
|
+
emitTrace('ask_human_call', {
|
|
663
|
+
tool: tc.name,
|
|
664
|
+
status: outcome.status,
|
|
665
|
+
perTurnCount: askHumanCountThisTurn,
|
|
666
|
+
}, tc.id ? { providerToolCallId: tc.id } : {});
|
|
667
|
+
}
|
|
668
|
+
const askBudget = askHumanBudgetAnnotation(askHumanCountThisTurn, outcome.status);
|
|
669
|
+
const finalOutput = askBudget ? `${annotated}${askBudget}` : annotated;
|
|
670
|
+
pushToolResult(history, tc, finalOutput, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
602
671
|
}
|
|
603
672
|
hooks.onToolDone?.();
|
|
604
673
|
i = j;
|
|
@@ -759,7 +828,31 @@ export async function runAgentCore(opts) {
|
|
|
759
828
|
hooks.onToolResult?.(tc, output, mutationParsed, diff.preWriteOld, diff.editStartLine);
|
|
760
829
|
// Thrashing:同上(history 附 hint,UI 干净)
|
|
761
830
|
const hint = recordAndHint(tc.name, tc.arguments, outcome.status === 'success');
|
|
762
|
-
|
|
831
|
+
// RETRY-01: 同 parallel 路径,在非成功状态追加反思指针。
|
|
832
|
+
const { output: annotated, reflectionCategory } = appendRetryAnnotations(output, outcome.status, outcome.code, hint);
|
|
833
|
+
// QUAL-01 trace 硬事件:反思指针注入计数(history 文本扫描会因 LLM 在
|
|
834
|
+
// 正文里提一句 "[retry reflection]" 而误算,改用 core 接缝处显式 emit)。
|
|
835
|
+
if (reflectionCategory !== null) {
|
|
836
|
+
emitTrace('retry_reflection', {
|
|
837
|
+
tool: tc.name,
|
|
838
|
+
code: outcome.code,
|
|
839
|
+
category: reflectionCategory,
|
|
840
|
+
attempt: outcome.attempts ?? 1,
|
|
841
|
+
}, tc.id ? { providerToolCallId: tc.id } : {});
|
|
842
|
+
}
|
|
843
|
+
// ASK-01: ask_human 计数 + 预算提示(同 parallel 路径)。
|
|
844
|
+
if (tc.name === 'ask_human' && outcome.status === 'success') {
|
|
845
|
+
askHumanCountThisTurn += 1;
|
|
846
|
+
// QUAL-01 trace 硬事件:ask_human 真实调用计数(取代 tool_call_end.name 推断)。
|
|
847
|
+
emitTrace('ask_human_call', {
|
|
848
|
+
tool: tc.name,
|
|
849
|
+
status: outcome.status,
|
|
850
|
+
perTurnCount: askHumanCountThisTurn,
|
|
851
|
+
}, tc.id ? { providerToolCallId: tc.id } : {});
|
|
852
|
+
}
|
|
853
|
+
const askBudget = askHumanBudgetAnnotation(askHumanCountThisTurn, outcome.status);
|
|
854
|
+
const finalOutput = askBudget ? `${annotated}${askBudget}` : annotated;
|
|
855
|
+
pushToolResult(history, tc, finalOutput, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
763
856
|
const invalidatedFiles = [...new Set([
|
|
764
857
|
...(outcome.changedFiles ?? []),
|
|
765
858
|
...(outcome.staleFiles ?? []),
|
|
@@ -899,6 +992,41 @@ export async function runAgentCore(opts) {
|
|
|
899
992
|
}
|
|
900
993
|
}
|
|
901
994
|
else {
|
|
995
|
+
// PROMPT-02: 硬关卡 — 收尾路径上,如果本 turn 有 mutation 且未通过
|
|
996
|
+
// validation(autoValidate=false / 验证已 passed 但 LLM 又宣称完成),
|
|
997
|
+
// 推一条 [checklist] user 消息,强制模型再走一轮显式确认。
|
|
998
|
+
// 防死循环:checklist 已推过 2 次仍无新工具调用 → 放行(走原本的 push)。
|
|
999
|
+
const finalMutationForChecklist = getCurrentTurnMutationState();
|
|
1000
|
+
const checklistCtx = {
|
|
1001
|
+
hadMutation: finalMutationForChecklist.version > 0,
|
|
1002
|
+
lastValidationStatus: (latestValidation?.status === 'failed' || latestValidation?.status === 'passed')
|
|
1003
|
+
? latestValidation.status
|
|
1004
|
+
: 'none',
|
|
1005
|
+
mode: getAgentMode(),
|
|
1006
|
+
modelFamily: inferModelFamily(config.model),
|
|
1007
|
+
};
|
|
1008
|
+
const checklistRetryCount = history.__checklistStreak ?? 0;
|
|
1009
|
+
const shouldFireChecklist = preCompletionChecklistHandler !== null
|
|
1010
|
+
&& preCompletionChecklistHandler(checklistCtx)
|
|
1011
|
+
&& checklistRetryCount < 2;
|
|
1012
|
+
if (shouldFireChecklist) {
|
|
1013
|
+
history.push(candidate);
|
|
1014
|
+
history.push({
|
|
1015
|
+
role: 'user',
|
|
1016
|
+
content: _checklistMiddleware.buildUserMessage(checklistCtx.modelFamily),
|
|
1017
|
+
});
|
|
1018
|
+
history.__checklistStreak = checklistRetryCount + 1;
|
|
1019
|
+
// QUAL-01 trace 硬事件:PROMPT-02 触发计数(取代 history 文本扫描
|
|
1020
|
+
// [checklist] marker — LLM 在正文里提一句 "[checklist]" 也会被误算)。
|
|
1021
|
+
emitTrace('checklist_triggered', {
|
|
1022
|
+
streak: checklistRetryCount + 1,
|
|
1023
|
+
validationStatus: checklistCtx.lastValidationStatus,
|
|
1024
|
+
hadMutation: checklistCtx.hadMutation,
|
|
1025
|
+
modelFamily: checklistCtx.modelFamily ?? 'other',
|
|
1026
|
+
});
|
|
1027
|
+
// 重要: 不置 done,让主循环继续下一轮(模型被迫用工具或写出可验证声明)。
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
902
1030
|
history.push(candidate);
|
|
903
1031
|
}
|
|
904
1032
|
const finalMutation = getCurrentTurnMutationState();
|
|
@@ -912,6 +1040,7 @@ export async function runAgentCore(opts) {
|
|
|
912
1040
|
usage: turnUsage,
|
|
913
1041
|
validation: latestValidation,
|
|
914
1042
|
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
1043
|
+
history: history.slice(),
|
|
915
1044
|
};
|
|
916
1045
|
}
|
|
917
1046
|
finally {
|
|
@@ -933,6 +1062,7 @@ export async function runAgentCore(opts) {
|
|
|
933
1062
|
usage: turnUsage,
|
|
934
1063
|
validation: latestValidation,
|
|
935
1064
|
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
1065
|
+
history: history.slice(),
|
|
936
1066
|
};
|
|
937
1067
|
}
|
|
938
1068
|
finally {
|
package/dist/agent/index.js
CHANGED
|
@@ -50,10 +50,12 @@ function writeChangeOverview() {
|
|
|
50
50
|
const changes = [...merged.values()];
|
|
51
51
|
const added = changes.reduce((n, c) => n + c.added, 0);
|
|
52
52
|
const removed = changes.reduce((n, c) => n + c.removed, 0);
|
|
53
|
-
layout.contentWrite(` ${ui.
|
|
53
|
+
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${t('agent.changes')} ${t('agent.files', { count: changes.length })} ${ui.green}+${added}${ui.reset} ${ui.red}−${removed}${ui.reset}\n`);
|
|
54
54
|
for (const change of changes) {
|
|
55
|
-
|
|
55
|
+
const kindLabel = change.kind === 'A' ? t('agent.changeAdded') : t('agent.changeModified');
|
|
56
|
+
layout.contentWrite(` ${kindLabel} ${change.path} ${ui.green}+${change.added}${ui.reset} ${ui.red}−${change.removed}${ui.reset}\n`);
|
|
56
57
|
}
|
|
58
|
+
layout.contentWrite('\n');
|
|
57
59
|
}
|
|
58
60
|
/** 取 userInput 的首行:字符串直接 split;多模态 parts 找首个 text part 再 split。 */
|
|
59
61
|
function firstLineOf(ui) {
|
|
@@ -228,7 +230,7 @@ onContextUpdate) {
|
|
|
228
230
|
},
|
|
229
231
|
onMaxSteps: () => {
|
|
230
232
|
flushToolBatch();
|
|
231
|
-
layout.contentWrite(` ${ui.
|
|
233
|
+
layout.contentWrite(` ${ui.accent}●${ui.reset} ${ui.yellow}${t('agent.maxSteps', { count: config.maxSteps })}${ui.reset}\n`);
|
|
232
234
|
},
|
|
233
235
|
onAbort: () => {
|
|
234
236
|
spinner.stop();
|
|
@@ -252,14 +254,14 @@ onContextUpdate) {
|
|
|
252
254
|
const detail = validation.status === 'skipped' && validation.skipReason
|
|
253
255
|
? `${validation.status}: ${validation.skipReason}`
|
|
254
256
|
: validation.status;
|
|
255
|
-
const symbol = validation.status === 'passed' ? '
|
|
256
|
-
layout.contentWrite(` ${
|
|
257
|
+
const symbol = validation.status === 'passed' ? '●' : validation.status === 'failed' ? '×' : '!';
|
|
258
|
+
layout.contentWrite(` ${color}${symbol}${ui.reset} ${t('agent.validationResult', { command, status: detail })}\n\n`);
|
|
257
259
|
},
|
|
258
260
|
onDone: (elapsedMs, usage) => {
|
|
259
261
|
flushToolBatch();
|
|
260
262
|
writeChangeOverview();
|
|
261
263
|
const tok = formatTurnTokens(usage);
|
|
262
|
-
layout.contentWrite(` ${ui.bold}${ui.
|
|
264
|
+
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${t('agent.complete')} ${fmtElapsed(elapsedMs)}${tok}\n`);
|
|
263
265
|
// 内容区触底时,DECSTBM 增量滚屏可能只推进物理终端,未把 Worked 前已在
|
|
264
266
|
// buffer 中的空行完整画出来;用户滚动/点击触发 repaint 后才“突然”出现。
|
|
265
267
|
// 轮次收尾立即按 buffer 原子重画,使未满屏与触底滚屏的布局一致。
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// PROMPT-02: PreCompletionChecklistMiddleware.
|
|
2
|
+
//
|
|
3
|
+
// 在 agent 宣告完成(no tool calls + 有 candidate 正文)的那一刻,向 history
|
|
4
|
+
// 推一条 user 消息,让模型再走一轮对 5 项 checklist 做显式确认 —— 这是
|
|
5
|
+
// "硬关卡",与 PROMPT-01 的 4 阶段"软纪律"叠加。
|
|
6
|
+
//
|
|
7
|
+
// 关键约束:
|
|
8
|
+
// - 纯函数(handler / buildUserMessage) → 易测、易 opt-out。
|
|
9
|
+
// - 5 项 checklist 是稳定的字符串数组,fixture 可直接断言关键词。
|
|
10
|
+
// - 复用 PROMPT-01 核心文本中的 "verification is a hard prerequisite" 锚点
|
|
11
|
+
// 风格(不另起炉灶),但用单数第一人称直白的 checklist 措辞。
|
|
12
|
+
// - 与 thrash-tracker 风格正交:trash 是 hint(LLM 仍可继续 retry),checklist
|
|
13
|
+
// 是 gate(LLM 必须覆盖才能 done)。
|
|
14
|
+
// - opt-out 显式:plan 模式 / 调用方传 `preCompletionChecklist: false`。
|
|
15
|
+
/** 5 项 checklist 内容(稳定,fixture 直接断言)。措辞对齐 LangChain 实证。 */
|
|
16
|
+
export const CHECKLIST_ITEMS = [
|
|
17
|
+
'Did I actually run a command/test/compile, or did I just re-read my own code?',
|
|
18
|
+
'Does the output match the original spec, not "what I thought I wrote"?',
|
|
19
|
+
'Did I exercise the boundary I claim to have covered (not just the happy path)?',
|
|
20
|
+
'Does the existing test suite still pass?',
|
|
21
|
+
'If I cannot verify, did I tell the user explicitly instead of pretending to be done?',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* 拼出 checklist user 消息。固定 5 项 + 硬规则;以 `[checklist]` 起头
|
|
25
|
+
* 便于 LLM 识别为强制二次确认(也便于 eval fixture 关键字定位)。
|
|
26
|
+
*/
|
|
27
|
+
export function buildChecklistUserMessage(modelFamily) {
|
|
28
|
+
const opener = modelFamily === 'anthropic'
|
|
29
|
+
? 'Verification gate — you have not actually run a command that exercises the change against the spec. Before declaring done, answer each item below explicitly:'
|
|
30
|
+
: modelFamily === 'openai'
|
|
31
|
+
? 'MANDATORY pre-completion checklist. You MUST address every item below before your final reply:'
|
|
32
|
+
: 'Pre-completion checklist — answer each item explicitly before declaring done:';
|
|
33
|
+
const items = CHECKLIST_ITEMS.map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
34
|
+
return `[checklist] ${opener}
|
|
35
|
+
|
|
36
|
+
${items}
|
|
37
|
+
|
|
38
|
+
Hard rule: "I read the code and it looks right" is not a completion signal. If you cannot run a verification, say so explicitly in your final reply. Do not paraphrase this checklist back as the answer — provide the actual evidence (which command, which output, which spec line it satisfied).
|
|
39
|
+
|
|
40
|
+
Bonus (ASK-01): Before declaring done, also answer: am I guessing any fact the user did not state? If yes, surface the guess to the user in your final reply or call \`ask_human\` (within the per-turn budget) — never silently guess on a non-reversible choice.`;
|
|
41
|
+
}
|
|
42
|
+
/** 默认 trigger 条件:有 mutation + 没工具调用 + 验证未通过或没跑过 + 非 plan。 */
|
|
43
|
+
export const defaultChecklistHandler = (ctx) => {
|
|
44
|
+
if (ctx.mode === 'plan')
|
|
45
|
+
return false;
|
|
46
|
+
if (!ctx.hadMutation)
|
|
47
|
+
return false;
|
|
48
|
+
// 已经通过验证:放行,不再二次确认(避免噪声)。
|
|
49
|
+
if (ctx.lastValidationStatus === 'passed')
|
|
50
|
+
return false;
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
export function createPreCompletionChecklistMiddleware() {
|
|
54
|
+
return {
|
|
55
|
+
handler: defaultChecklistHandler,
|
|
56
|
+
buildUserMessage: buildChecklistUserMessage,
|
|
57
|
+
items: CHECKLIST_ITEMS,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// RETRY-01: 错误反思分类器。
|
|
2
|
+
//
|
|
3
|
+
// 在 tools/retry.ts 之上叠加一层"反思"语义:把每个 ToolOutcomeCode 映射到
|
|
4
|
+
// 一个 ErrorCategory,供 (a) PROMPT 反思 prompt 注入、(b) QUAL-01 trace 指标。
|
|
5
|
+
//
|
|
6
|
+
// 设计要点:
|
|
7
|
+
// - 纯映射函数,无副作用 → 易测,易扩展(新 code → 改一处)。
|
|
8
|
+
// - 6 类对齐路线图 L163:`TRANSIENT_RATE_LIMIT` / `TRANSIENT_TIMEOUT` /
|
|
9
|
+
// `INVALID_ARGUMENTS` / `PERMISSION_DENIED` / `CONFLICT` / `UNKNOWN_FAILURE`。
|
|
10
|
+
// - 提供"反思指针"模板(reflectionHint),与 PROMPT-01 hard rule
|
|
11
|
+
// / PROMPT-02 checklist 共用同一套"先想清楚再动"语言,避免新造话。
|
|
12
|
+
// - 不**决策**重试与否(`shouldRetry` 在 tools/retry.ts 已经稳定;本次不动),
|
|
13
|
+
// 只**注解**每个错误"该反思什么"。
|
|
14
|
+
/** code → category 的稳定映射。fixture 直接断言(避免漏改)。 */
|
|
15
|
+
const CODE_TO_CATEGORY = {
|
|
16
|
+
OK: 'UNKNOWN_FAILURE', // OK 不该走到这里;归 UNKNOWN 是为了编译期不漏。
|
|
17
|
+
// rate-limit / timeout / network
|
|
18
|
+
TIMEOUT: 'TRANSIENT_TIMEOUT',
|
|
19
|
+
HTTP_ERROR: 'TRANSIENT_RATE_LIMIT', // 4xx/5xx 中除 408/429 之外的 5xx 视为 rate-limit 语义
|
|
20
|
+
NETWORK_ERROR: 'TRANSIENT_TIMEOUT', // 网络层错(ECONNRESET 等)→ 与 timeout 同类反思
|
|
21
|
+
// 参数 / schema(模型自己改,不是 retry 自己改)
|
|
22
|
+
INVALID_JSON: 'INVALID_ARGUMENTS',
|
|
23
|
+
INVALID_ARGUMENTS: 'INVALID_ARGUMENTS',
|
|
24
|
+
INVALID_TOOL_SCHEMA: 'INVALID_ARGUMENTS',
|
|
25
|
+
UNKNOWN_TOOL: 'INVALID_ARGUMENTS', // 调用了不存在的工具 → 模型改 tool name
|
|
26
|
+
// 权限 / 沙箱 / 模式(永久拒绝)
|
|
27
|
+
SANDBOX_DENIED: 'PERMISSION_DENIED',
|
|
28
|
+
PERMISSION_DENIED: 'PERMISSION_DENIED',
|
|
29
|
+
TOOL_DISABLED: 'PERMISSION_DENIED',
|
|
30
|
+
MODE_DENIED: 'PERMISSION_DENIED',
|
|
31
|
+
ABORTED: 'PERMISSION_DENIED', // 用户主动 abort,与 permission 同类(无 retry 价值)
|
|
32
|
+
// 冲突 / 写失败(retry 之前必须重新读)
|
|
33
|
+
EDIT_CONFLICT: 'CONFLICT',
|
|
34
|
+
CHANGE_CONFLICT: 'CONFLICT',
|
|
35
|
+
PATCH_INVALID: 'CONFLICT',
|
|
36
|
+
POSTCONDITION_FAILED: 'CONFLICT',
|
|
37
|
+
PROCESS_FAILED: 'CONFLICT',
|
|
38
|
+
// 兜底
|
|
39
|
+
EXECUTION_ERROR: 'UNKNOWN_FAILURE',
|
|
40
|
+
MCP_ERROR: 'UNKNOWN_FAILURE',
|
|
41
|
+
};
|
|
42
|
+
export function classifyError(code) {
|
|
43
|
+
return CODE_TO_CATEGORY[code];
|
|
44
|
+
}
|
|
45
|
+
/** 反思指针:每类错误给 LLM 一句"反思什么"的话。 */
|
|
46
|
+
const REFLECTION_HINTS = {
|
|
47
|
+
TRANSIENT_RATE_LIMIT: 'The remote is throttling or returned 5xx. Re-running the same call with identical arguments is unlikely to help — wait, downgrade call frequency, or reduce request size; do not retry the same args in tight loop.',
|
|
48
|
+
TRANSIENT_TIMEOUT: 'A network or process timeout occurred. Re-running the same call may help once, but if it fails again with identical args, the path/host/argument is wrong; switch tool, reduce scope, or ask the user.',
|
|
49
|
+
INVALID_ARGUMENTS: 'The tool rejected the call shape (JSON / schema / unknown tool). Read the tool description again and fix the arguments yourself; do not resend the same call and do not retry automatically — only the model can fix argument shape.',
|
|
50
|
+
PERMISSION_DENIED: 'The call was denied (sandbox / permission / disabled / mode). Retry will not help; surface the constraint to the user and ask for guidance or a permission grant.',
|
|
51
|
+
CONFLICT: 'A write/patch conflicted with the on-disk state. Do not resend the same args; re-read the file or change target, then re-derive the diff. If the conflict is structural, the plan itself is wrong — go back to the spec.',
|
|
52
|
+
UNKNOWN_FAILURE: 'An unclassified error occurred. Do not retry blindly; capture the message, re-read the tool description, and decide whether the same call with a smaller scope or a different tool is appropriate. If unsure, ask the user.',
|
|
53
|
+
};
|
|
54
|
+
export function reflectionHint(category) {
|
|
55
|
+
return REFLECTION_HINTS[category];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 一站式:对 ToolOutcome 给出"反思 category + 反思 hint"。
|
|
59
|
+
* 调用方一般是 tools/retry.ts 的 onRetry hook,把 hint 注入 retry 提示尾部。
|
|
60
|
+
*/
|
|
61
|
+
export function reflectOn(code) {
|
|
62
|
+
const category = classifyError(code);
|
|
63
|
+
return { category, hint: reflectionHint(category) };
|
|
64
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// PROMPT-01: Build-and-Self-Verify working discipline.
|
|
2
|
+
//
|
|
3
|
+
// 把"完成必须验证"作为 system prompt 的一等公民,而不是事后选项。注入一段
|
|
4
|
+
// 4 阶段纪律(Plan & Discover → Build → Verify → Fix),并提供 per-model
|
|
5
|
+
// 措辞(anthropic / openai / qwen)让 base model 拿到最适合自己的表述。
|
|
6
|
+
//
|
|
7
|
+
// 关键约束:
|
|
8
|
+
// - 纯函数,无副作用,无 config 依赖 → 不踩 TDZ,易测,易回滚。
|
|
9
|
+
// - 段标题在 buildMocodeCorePrompt 之外,不会被 `## Project context` 索引
|
|
10
|
+
// 切片误伤;且 buildBasePrompt 注入位置在 ## Workflow 之前,确保 LLM
|
|
11
|
+
// 先看到纪律再看工具/平台细节。
|
|
12
|
+
// - per-model 措辞是"轻量"差异:3 个家族共享 4 阶段结构,只在首句/标签
|
|
13
|
+
// 上贴近该家族的指令遵从习惯;真正的 prompt 反演化交给 AHE。
|
|
14
|
+
// - 语种统一英文:4 份都用同一份核心纪律文本,避免多语种漂移;用户语言
|
|
15
|
+
// 偏好由现有 i18n 段(assistant.languageInstruction)负责。
|
|
16
|
+
/**
|
|
17
|
+
* 从 config.model 字符串里嗅探 model family。匹配规则尽量宽松,够用即可。
|
|
18
|
+
* 未来 AHE 闭环后可以换成 config.modelFamily 字段。
|
|
19
|
+
*/
|
|
20
|
+
export function inferModelFamily(model) {
|
|
21
|
+
if (!model)
|
|
22
|
+
return 'other';
|
|
23
|
+
const m = model.toLowerCase();
|
|
24
|
+
if (m.includes('claude') || m.includes('anthropic'))
|
|
25
|
+
return 'anthropic';
|
|
26
|
+
if (m.includes('gpt') || m.includes('o1') || m.includes('o3') || m.includes('openai'))
|
|
27
|
+
return 'openai';
|
|
28
|
+
if (m.includes('qwen') || m.includes('qwq') || m.includes('tongyi'))
|
|
29
|
+
return 'qwen';
|
|
30
|
+
return 'other';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 4 阶段核心纪律(英文)。4 个 model family 共用此文本,只在首句与标题
|
|
34
|
+
* 标签上做轻量变体。长度 ~380 词,确保不显著抬升 token 预算。
|
|
35
|
+
*/
|
|
36
|
+
const CORE_SECTION = `## Working discipline — coding tasks (Build-and-Self-Verify)
|
|
37
|
+
|
|
38
|
+
Treat "verification" as a first-class part of the task, not an afterthought. Every coding task runs through four sequential phases; skipping or merging phases is a failure mode.
|
|
39
|
+
|
|
40
|
+
### Phase 1 — Plan & Discover
|
|
41
|
+
- Restate the goal in one sentence; identify the acceptance signal (test name, command output, file presence, behavior change).
|
|
42
|
+
- Read the relevant code BEFORE writing anything; record assumptions you cannot verify (write them down, do not silently assume).
|
|
43
|
+
- If the spec is ambiguous, surface the ambiguity to the user via \`ask_human\` before implementing — do not guess on irreversible choices (deletions, public API changes, schema/permission boundaries).
|
|
44
|
+
|
|
45
|
+
### Phase 2 — Build
|
|
46
|
+
- Make the smallest change that satisfies the spec. Do not bundle unrelated refactors.
|
|
47
|
+
- If the project has tests, your change is incomplete without a test for the new/changed behavior. "Should" clauses in the goal are obligations, not aspirations.
|
|
48
|
+
- After every mutation, re-read the relevant region (snapshot drift is real — your memory of the file is stale after the previous edit).
|
|
49
|
+
|
|
50
|
+
### Phase 3 — Verify
|
|
51
|
+
- Run a real, executable verification: typecheck, the project's test command, a focused command that exercises the change, or a smoke script. Read the full output, not just the last line.
|
|
52
|
+
- Compare the result to the SPEC, not to your own diff. A diff that "looks right" against itself is not evidence.
|
|
53
|
+
- If the project has no test infra you can use, build the smallest possible reproducer (a script, a focused command) that exercises the change. "I read the code and it looks correct" is not verification.
|
|
54
|
+
|
|
55
|
+
### Phase 4 — Fix
|
|
56
|
+
- Any failure → go back to the spec, not to the diff. Re-derive what the spec requires; do not "tweak" the implementation to silence the failing test.
|
|
57
|
+
- After a fix, re-run Phase 3 end-to-end. Do not declare done on a single passing run after multiple failed ones unless you understand and can name the root cause of every previous failure.
|
|
58
|
+
- Cap blind retries: after three identical failed attempts on the same tool with the same arguments, change the approach (different tool, different invariant, or \`ask_human\`) instead of retrying.
|
|
59
|
+
|
|
60
|
+
**Hard rule (non-negotiable):** "I read the code and it looks right" is not a completion signal. A task is complete only when an executable verification against the spec has actually run, its full output has been read, and the result matches the spec. Report this evidence explicitly in your final reply (which command, which output, which spec line it satisfied).`;
|
|
61
|
+
/**
|
|
62
|
+
* 把核心段适配到指定 model family:只改首行(语序 / 强动词)与段标题
|
|
63
|
+
* 末尾的 [model: X] 标签。Phase 内容保持原样,4 份共享同一份结构化文本。
|
|
64
|
+
*/
|
|
65
|
+
function adapt(model, opener) {
|
|
66
|
+
return CORE_SECTION
|
|
67
|
+
.replace('## Working discipline — coding tasks (Build-and-Self-Verify)', `## Working discipline — coding tasks (Build-and-Self-Verify) [model: ${model}]`)
|
|
68
|
+
.replace('Treat "verification" as a first-class part of the task, not an afterthought.', opener);
|
|
69
|
+
}
|
|
70
|
+
/** ASK-01: 卡点白名单段。PROMPT-01 4 阶段纪律之后追加,告诉模型在哪些
|
|
71
|
+
* 边界情形应当优先调 `ask_human`,而不是猜测。语种统一英文(与 PROMPT-01
|
|
72
|
+
* 保持一致,避免多语种漂移);5 个固定条目,与 checklist 第 6 项耦合。
|
|
73
|
+
*/
|
|
74
|
+
const ASK_WHITELIST_SECTION = `## When to ask instead of guess
|
|
75
|
+
|
|
76
|
+
The following situations are not guessable. When you encounter any of them, call \`ask_human\` BEFORE writing code; do not silently pick one option and proceed.
|
|
77
|
+
|
|
78
|
+
1. **Cross-package impact** — the change touches public APIs, exports, or interfaces of other packages/modules; the user must confirm the blast radius.
|
|
79
|
+
2. **Naming conventions** — the project has no obvious style for this artifact (e.g. new file in a folder with no precedent); naming is cheap to fix and expensive to mass-rename later.
|
|
80
|
+
3. **Keep or remove old API** — the change deprecates, renames, or removes a function/type; the user must decide.
|
|
81
|
+
4. **Test expectations** — the spec says "should work" or "should handle" but does not pin down the input/output contract; ask for a concrete example or assertion.
|
|
82
|
+
5. **Implicit success criteria** — the user described intent but not the verification signal (which command, which output, which line of the spec). Without this, you cannot run Phase 3 honestly.
|
|
83
|
+
|
|
84
|
+
Budget: at most 2 \`ask_human\` calls per turn. If you would exceed the budget, prefer the safer default (e.g. "preserve old behavior" / "add a test, do not change behavior") and explicitly disclose the choice in your final reply — do not silently guess without disclosure. The disclosure is what the checklist item 6 is about.`;
|
|
85
|
+
/**
|
|
86
|
+
* 拼出纪律段 + ASK-01 卡点白名单。返回完整段(两段用 \`\\n\\n\` 隔开);
|
|
87
|
+
* 工厂之前只返回纪律段,ASK-01 落地后变成纪律 + 白名单两段;
|
|
88
|
+
* ASK-01 段是固定英文,不参与 per-model 适配(避免 4 份变体维护成本)。
|
|
89
|
+
*/
|
|
90
|
+
export function buildWorkDisciplineSection(modelFamily) {
|
|
91
|
+
let section;
|
|
92
|
+
switch (modelFamily) {
|
|
93
|
+
case 'anthropic':
|
|
94
|
+
section = adapt('anthropic', 'Verification is a hard prerequisite for completion, not a courtesy.');
|
|
95
|
+
break;
|
|
96
|
+
case 'openai':
|
|
97
|
+
section = adapt('openai', 'Every coding task MUST complete these four phases in order. Skipping or merging phases is treated as a failure.');
|
|
98
|
+
break;
|
|
99
|
+
case 'qwen':
|
|
100
|
+
section = adapt('qwen', 'Verification is a hard prerequisite for completion; "I wrote the code" is not evidence the code works.');
|
|
101
|
+
break;
|
|
102
|
+
case 'other':
|
|
103
|
+
case undefined:
|
|
104
|
+
default:
|
|
105
|
+
section = CORE_SECTION;
|
|
106
|
+
}
|
|
107
|
+
return `${section}\n\n${ASK_WHITELIST_SECTION}`;
|
|
108
|
+
}
|
package/dist/config/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { loadSnapshot } from '../project-snapshot/index.js';
|
|
|
6
6
|
import { buildProjectSkillSection } from '../project-skill/index.js';
|
|
7
7
|
import { getSandboxRoot } from '../sandbox/root.js';
|
|
8
8
|
import { getCurrentSessionId } from '../session/state.js';
|
|
9
|
+
import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
|
|
9
10
|
import { detectLanguage, setLanguage, t, } from '../i18n/index.js';
|
|
10
11
|
/**
|
|
11
12
|
* 按优先级加载配置文件并回填 process.env:
|
|
@@ -209,6 +210,8 @@ ${planLine}
|
|
|
209
210
|
|
|
210
211
|
${PLATFORM_NOTE}
|
|
211
212
|
|
|
213
|
+
${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
214
|
+
|
|
212
215
|
## Tool details
|
|
213
216
|
### Token-efficient execution
|
|
214
217
|
- First check whether the answer is already in this conversation or a previous tool result. If yes, answer directly; do not re-run tools "to be safe".
|
|
@@ -228,7 +231,7 @@ ${PLATFORM_NOTE}
|
|
|
228
231
|
|
|
229
232
|
## Tool rules
|
|
230
233
|
- Precise path/symbol → go directly to \`read_file\` or \`codegraph node\`; use \`glob\`/\`grep\` only for discovery.
|
|
231
|
-
- Before editing, read the exact target region and copy both its artifact \`hash\` and verbatim text. Use \`edit_file\` with \`expected_hash\` for unique local replacements, and \`write_file\` with the latest hash for replacement (or null only for creation).
|
|
234
|
+
- Before editing, read the exact target region and copy both its artifact \`hash\` and verbatim text. Use \`edit_file\` with \`expected_hash\` for unique local replacements, and \`write_file\` with the latest hash for replacement (or null only for creation).
|
|
232
235
|
- Local edits require an exact unique match; use \`write_file\` for new/full files.
|
|
233
236
|
- Use \`glob\`/\`grep\` for discovery and \`run_command\` for execution or verification, not file existence checks. State intent before side effects.
|
|
234
237
|
- Call \`ask_human\` only when a real user decision is required; otherwise decide and proceed.
|
|
@@ -251,7 +254,7 @@ ${PLATFORM_NOTE}
|
|
|
251
254
|
- Operate only within authorized scope; when unsure, ask — don't guess.
|
|
252
255
|
|
|
253
256
|
## Project context (dynamic reference)
|
|
254
|
-
${buildSnapshotSection()}${config.projectSkillEnabled ? buildProjectSkillSection() : ''}${memorySection}${buildNotepadSection(sessionId)}
|
|
257
|
+
${buildSnapshotSection()}${config.projectSkillEnabled ? buildProjectSkillSection() : ''}${memorySection}${buildNotepadSection(sessionId)}
|
|
255
258
|
|
|
256
259
|
## Session Notepad — working notes file
|
|
257
260
|
${sessionId
|
package/dist/foo.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a semver string into its numeric major/minor/patch triple.
|
|
3
|
+
*
|
|
4
|
+
* Accepts the two shapes the caller asked about:
|
|
5
|
+
* - "1.2.3"
|
|
6
|
+
* - "v2.0.0-rc.1"
|
|
7
|
+
*
|
|
8
|
+
* Prerelease / build metadata are accepted after the patch number, but only
|
|
9
|
+
* the three numeric core components are returned. Anything that does not
|
|
10
|
+
* match `^v?MAJOR.MINOR.PATCH(-prerelease)?(+build)?` raises an Error whose
|
|
11
|
+
* message contains "invalid semver".
|
|
12
|
+
*/
|
|
13
|
+
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
14
|
+
export function parseSemver(input) {
|
|
15
|
+
if (typeof input !== 'string') {
|
|
16
|
+
throw new Error(`invalid semver: not a string: ${String(input)}`);
|
|
17
|
+
}
|
|
18
|
+
const m = SEMVER_RE.exec(input);
|
|
19
|
+
if (!m) {
|
|
20
|
+
throw new Error(`invalid semver: ${JSON.stringify(input)}`);
|
|
21
|
+
}
|
|
22
|
+
// Numeric groups are guaranteed by the \d+ pattern; Number() won't produce NaN.
|
|
23
|
+
return {
|
|
24
|
+
major: Number(m[1]),
|
|
25
|
+
minor: Number(m[2]),
|
|
26
|
+
patch: Number(m[3]),
|
|
27
|
+
};
|
|
28
|
+
}
|
package/dist/foo.test.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { parseSemver } from './foo.js';
|
|
4
|
+
test('parseSemver parses plain x.y.z', () => {
|
|
5
|
+
assert.deepEqual(parseSemver('1.2.3'), { major: 1, minor: 2, patch: 3 });
|
|
6
|
+
});
|
|
7
|
+
test('parseSemver parses v-prefixed prerelease', () => {
|
|
8
|
+
assert.deepEqual(parseSemver('v2.0.0-rc.1'), { major: 2, minor: 0, patch: 0 });
|
|
9
|
+
});
|
|
10
|
+
test('parseSemver throws "invalid semver" on garbage input', () => {
|
|
11
|
+
assert.throws(() => parseSemver('not-a-version'), /invalid semver/);
|
|
12
|
+
});
|
package/dist/i18n/index.js
CHANGED
|
@@ -174,6 +174,8 @@ const zhCN = {
|
|
|
174
174
|
'agent.toolsFailed': '探索失败',
|
|
175
175
|
'agent.changes': '文件变更',
|
|
176
176
|
'agent.files': '{count} 个文件',
|
|
177
|
+
'agent.changeAdded': ' 新增',
|
|
178
|
+
'agent.changeModified': ' 修改',
|
|
177
179
|
'agent.complete': '完成',
|
|
178
180
|
'toolSummary.lines': '{count} 行',
|
|
179
181
|
'toolSummary.files': '{count} 个文件',
|
|
@@ -406,6 +408,8 @@ const en = {
|
|
|
406
408
|
'agent.toolsFailed': 'Exploration failed',
|
|
407
409
|
'agent.changes': 'Changes',
|
|
408
410
|
'agent.files': '{count} file(s)',
|
|
411
|
+
'agent.changeAdded': ' Added',
|
|
412
|
+
'agent.changeModified': ' Modified',
|
|
409
413
|
'agent.complete': 'Complete',
|
|
410
414
|
'toolSummary.lines': '{count} lines',
|
|
411
415
|
'toolSummary.files': '{count} files',
|
package/dist/repl/index.js
CHANGED
|
@@ -2055,7 +2055,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
2055
2055
|
const initPrompt = `请直接初始化或优化当前项目的 Project Skill,并完成写入。
|
|
2056
2056
|
|
|
2057
2057
|
要求:
|
|
2058
|
-
1. 直接由你完成,禁止调用 sub-agent 工具或派生任何子 agent。
|
|
2058
|
+
1. 直接由你完成,禁止调用 sub-agent 工具或派生任何子 agent。
|
|
2059
2059
|
2. 优先利用系统提示中已有的 Project Snapshot 和 Project Skill;不要重复扫描其中已有的目录、依赖、命令和模块清单。
|
|
2060
2060
|
3. 最多进行 1 次 codegraph 探索;只有缺少关键依据时,才额外进行少量定点 read_file/grep。禁止全仓 glob 和逐文件扫描。
|
|
2061
2061
|
4. Skill 只记录 Snapshot 无法提供的 WHY/HOW/GOTCHAS/CONVENTIONS:设计取舍、关键调用链、非直觉边界、项目约定和可操作坑点。使用具体路径和例子,删除重复或过时内容。
|
|
@@ -39,7 +39,7 @@ export function createBudgetScheduler(state = contextState) {
|
|
|
39
39
|
for (const a of actions) {
|
|
40
40
|
if (a.kind === 'warn') {
|
|
41
41
|
// system 超:写一行提示(配置漂移应由用户处理,不是调度器压)
|
|
42
|
-
layout.contentWrite(` ${ui.
|
|
42
|
+
layout.contentWrite(` ${ui.accent}●${ui.reset} ${ui.yellow}调度器警告 [${a.layer}] ${a.reason}${ui.reset}\n`);
|
|
43
43
|
}
|
|
44
44
|
else if (a.kind === 'compact_history') {
|
|
45
45
|
// 路由到 maybeCompact;把结构重建信号传回 core,使 lifecycle 按新 index 恢复。
|
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
|
|
2
|
+
/** 把 history 中所有 user/tool/assistant 消息的文本/参数汇总到一个大字符串,便于
|
|
3
|
+
* 用单一正则扫描多个 marker。比逐条消息 join 更稳:LLM 流式 contentPart 数组
|
|
4
|
+
* 与多模态混合时,joinStringField 内部把每个 part 展平成字符串。 */
|
|
5
|
+
function joinHistoryForMarkerScan(history) {
|
|
6
|
+
if (!history)
|
|
7
|
+
return '';
|
|
8
|
+
const parts = [];
|
|
9
|
+
for (const message of history) {
|
|
10
|
+
if (typeof message.content === 'string') {
|
|
11
|
+
parts.push(message.content);
|
|
12
|
+
}
|
|
13
|
+
else if (Array.isArray(message.content)) {
|
|
14
|
+
for (const part of message.content) {
|
|
15
|
+
if (typeof part === 'string')
|
|
16
|
+
parts.push(part);
|
|
17
|
+
else if (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') {
|
|
18
|
+
parts.push(part.text);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if ('tool_call_id' in message && typeof message.tool_call_id === 'string') {
|
|
23
|
+
parts.push(message.tool_call_id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return parts.join('\n');
|
|
27
|
+
}
|
|
28
|
+
/** QUAL-01 质量维度。
|
|
29
|
+
*
|
|
30
|
+
* 优先用硬事件(retry_reflection / checklist_triggered / ask_human_call),
|
|
31
|
+
* 这是 core.ts 在接缝处显式 emit 的,可信度高。
|
|
32
|
+
* 如果 events 里没有对应硬事件(老 trace / 旧 fixture 走 history 文本回放),fallback
|
|
33
|
+
* 到 history 文本扫描 + tool_call_end(name === 'ask_human', status === 'success')。
|
|
34
|
+
*
|
|
35
|
+
* - `retry_reflection` → 反思重试注入次数(RETRY-01)
|
|
36
|
+
* - `checklist_triggered` → PROMPT-02 触发次数
|
|
37
|
+
* - `ask_human_call` (status === 'success') → ASK-01 触发次数
|
|
38
|
+
*
|
|
39
|
+
* 三类都给出 fallback 0,确保报告维度永远是 number,不会被 NaN 污染。 */
|
|
40
|
+
function reduceQualityDimensions(events, history) {
|
|
41
|
+
// 1. 硬事件计数(优先级最高)。
|
|
42
|
+
let reflectionRounds = 0;
|
|
43
|
+
let checklistTriggered = 0;
|
|
44
|
+
let askHumanCount = 0;
|
|
45
|
+
let hasHardReflection = false;
|
|
46
|
+
let hasHardChecklist = false;
|
|
47
|
+
let hasHardAskHuman = false;
|
|
48
|
+
for (const event of events) {
|
|
49
|
+
if (event.type === 'retry_reflection') {
|
|
50
|
+
reflectionRounds += 1;
|
|
51
|
+
hasHardReflection = true;
|
|
52
|
+
}
|
|
53
|
+
else if (event.type === 'checklist_triggered') {
|
|
54
|
+
checklistTriggered += 1;
|
|
55
|
+
hasHardChecklist = true;
|
|
56
|
+
}
|
|
57
|
+
else if (event.type === 'ask_human_call' && event.data.status === 'success') {
|
|
58
|
+
askHumanCount += 1;
|
|
59
|
+
hasHardAskHuman = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 2. fallback: 历史事件没有硬信号时,从 history 文本 + tool_call_end 推断。
|
|
63
|
+
// 保留旧 fixture / 回放 JSONL 的兼容能力。
|
|
64
|
+
if (!hasHardReflection || !hasHardChecklist || !hasHardAskHuman) {
|
|
65
|
+
if (!hasHardReflection) {
|
|
66
|
+
const historyText = joinHistoryForMarkerScan(history);
|
|
67
|
+
reflectionRounds += (historyText.match(/\[retry reflection:/g) ?? []).length;
|
|
68
|
+
}
|
|
69
|
+
if (!hasHardChecklist) {
|
|
70
|
+
const historyText = joinHistoryForMarkerScan(history);
|
|
71
|
+
checklistTriggered += (historyText.match(/\[checklist\]/g) ?? []).length;
|
|
72
|
+
}
|
|
73
|
+
if (!hasHardAskHuman) {
|
|
74
|
+
for (const event of events) {
|
|
75
|
+
if (event.type !== 'tool_call_end')
|
|
76
|
+
continue;
|
|
77
|
+
if (event.data.name === 'ask_human' && event.data.status === 'success') {
|
|
78
|
+
askHumanCount += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { reflectionRounds, askHumanCount, checklistTriggered };
|
|
84
|
+
}
|
|
85
|
+
export function reduceTraceMetrics(events, history) {
|
|
3
86
|
const ends = events.filter((event) => event.type === 'tool_call_end');
|
|
4
87
|
let recovered = false;
|
|
5
88
|
let hadFailure = false;
|
|
@@ -32,6 +115,7 @@ export function reduceTraceMetrics(events) {
|
|
|
32
115
|
const modelRetries = events.filter((event) => event.type === 'model_retry').length;
|
|
33
116
|
const firstValidation = events.find((event) => event.type === 'validation_end');
|
|
34
117
|
const turnEnd = [...events].reverse().find((event) => event.type === 'turn_end');
|
|
118
|
+
const quality = reduceQualityDimensions(events, history);
|
|
35
119
|
return {
|
|
36
120
|
toolCalls: events.filter((event) => event.type === 'tool_call_start').length,
|
|
37
121
|
toolFailures: ends.length - successes,
|
|
@@ -43,6 +127,7 @@ export function reduceTraceMetrics(events) {
|
|
|
43
127
|
tokens: hasTokens ? tokens : null,
|
|
44
128
|
durationMs: Number(turnEnd?.data.durationMs ?? 0),
|
|
45
129
|
firstValidationPassed: firstValidation?.data.status === 'passed',
|
|
130
|
+
...quality,
|
|
46
131
|
};
|
|
47
132
|
}
|
|
48
133
|
/** Reads event JSONL; malformed/legacy summary lines are ignored. */
|
|
@@ -6,7 +6,7 @@ function optionToChoice(o) {
|
|
|
6
6
|
if (o === null || o === undefined)
|
|
7
7
|
return { label: '' };
|
|
8
8
|
if (typeof o === 'string')
|
|
9
|
-
return { label: o };
|
|
9
|
+
return { label: o.trim() };
|
|
10
10
|
if (typeof o === 'number' || typeof o === 'boolean')
|
|
11
11
|
return { label: String(o) };
|
|
12
12
|
if (typeof o === 'object') {
|
|
@@ -89,14 +89,31 @@ export function coerceOptions(raw) {
|
|
|
89
89
|
}
|
|
90
90
|
return [];
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* 在通用 JSON Schema 校验前统一模型的常见错参。
|
|
94
|
+
* - 兼容历史字符串选项和 description/detail 等别名;
|
|
95
|
+
* - 丢弃 `{}` 等无可读内容的项;
|
|
96
|
+
* - 少于两个有效选项时统一为 `options: []`,明确表示自由文本面板。
|
|
97
|
+
*/
|
|
98
|
+
export function normalizeAskHumanArguments(args) {
|
|
99
|
+
const options = coerceOptions(args.options);
|
|
100
|
+
if (options.length < 2) {
|
|
101
|
+
args.options = [];
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
args.options = options.slice(0, 4).map((option) => ({
|
|
105
|
+
label: option.label,
|
|
106
|
+
...(option.detail ? { description: option.detail } : {}),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
92
109
|
// ---------- ask_human ----------
|
|
93
110
|
export const askHumanTool = {
|
|
94
111
|
name: 'ask_human',
|
|
95
112
|
description: [
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
'
|
|
113
|
+
'Ask the user a question and wait for their response.',
|
|
114
|
+
' CHOICES: pass 2-4 concrete options via `options`, each as { label, description? }.',
|
|
115
|
+
' FREE-TEXT: pass `options: []` when the answer cannot be reduced to choices (e.g. "paste the error message").',
|
|
116
|
+
' Every non-empty option requires a non-empty `label`; never emit [{}], omit label, or omit `options`.',
|
|
100
117
|
' DO NOT call when the task is clear and you can pick a sensible default.',
|
|
101
118
|
].join(' '),
|
|
102
119
|
parameters: {
|
|
@@ -104,39 +121,39 @@ export const askHumanTool = {
|
|
|
104
121
|
properties: {
|
|
105
122
|
question: {
|
|
106
123
|
type: 'string',
|
|
124
|
+
minLength: 1,
|
|
107
125
|
description: 'The question to ask the user; keep it concise (shown as the panel title)',
|
|
108
126
|
},
|
|
109
127
|
options: {
|
|
110
128
|
type: 'array',
|
|
129
|
+
minItems: 0,
|
|
130
|
+
maxItems: 4,
|
|
111
131
|
items: {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
type: '
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
description: {
|
|
123
|
-
type: 'string',
|
|
124
|
-
description: 'What picking this option means or implies; explain the tradeoff when the label alone leaves the user unsure.',
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
required: ['label'],
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
label: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
minLength: 1,
|
|
137
|
+
description: 'Short option title (1-5 words), shown as the choice itself.',
|
|
138
|
+
},
|
|
139
|
+
description: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
description: 'What picking this option means or implies; explain the tradeoff when the label alone leaves the user unsure.',
|
|
128
142
|
},
|
|
129
|
-
|
|
143
|
+
},
|
|
144
|
+
required: ['label'],
|
|
145
|
+
additionalProperties: false,
|
|
130
146
|
},
|
|
131
|
-
description: '
|
|
147
|
+
description: 'Pass [] for free-text input, or 2-4 concrete choices with non-empty labels.',
|
|
132
148
|
},
|
|
133
149
|
context: {
|
|
134
150
|
type: 'string',
|
|
135
151
|
description: 'Background explanation shown under the title; may be multiline.',
|
|
136
152
|
},
|
|
137
153
|
},
|
|
138
|
-
required: ['question'],
|
|
154
|
+
required: ['question', 'options'],
|
|
139
155
|
},
|
|
156
|
+
normalizeArguments: normalizeAskHumanArguments,
|
|
140
157
|
async execute(args) {
|
|
141
158
|
const question = String(args.question ?? '');
|
|
142
159
|
const options = coerceOptions(args.options);
|
package/dist/tools/retry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reflectOn } from '../agent/retry-classifier.js';
|
|
1
2
|
export const TOOL_RETRY_MAX_ATTEMPTS = 3;
|
|
2
3
|
export const TOOL_RETRY_BASE_MS = 250;
|
|
3
4
|
export const TOOL_RETRY_MAX_MS = 1_000;
|
|
@@ -68,11 +69,13 @@ function sleep(ms, signal) {
|
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
/** Retry safe/idempotent transient outcomes; each execute call owns one complete lock attempt. */
|
|
71
|
-
export async function executeWithToolRetry(capabilities, fingerprint, signal, execute, onRetry) {
|
|
72
|
+
export async function executeWithToolRetry(capabilities, fingerprint, signal, execute, onRetry, onFailedAttempt) {
|
|
72
73
|
const startedAt = Date.now();
|
|
73
74
|
let retryDelayMs = 0;
|
|
75
|
+
let lastOutcome = null;
|
|
74
76
|
for (let attempt = 1; attempt <= TOOL_RETRY_MAX_ATTEMPTS; attempt++) {
|
|
75
77
|
const outcome = await execute(attempt);
|
|
78
|
+
lastOutcome = outcome;
|
|
76
79
|
const elapsed = Date.now() - startedAt;
|
|
77
80
|
const waitMs = backoff(attempt);
|
|
78
81
|
const canRetry = attempt < TOOL_RETRY_MAX_ATTEMPTS &&
|
|
@@ -81,6 +84,25 @@ export async function executeWithToolRetry(capabilities, fingerprint, signal, ex
|
|
|
81
84
|
!signal?.aborted &&
|
|
82
85
|
reserveFingerprintRetry(fingerprint, Date.now());
|
|
83
86
|
if (!canRetry) {
|
|
87
|
+
// RETRY-02 接缝:所有 attempt 用尽时,如果调用方启用了 onFailedAttempt,
|
|
88
|
+
// 通知一次(用于 history 裁剪 / 等)。只在**真**失败(not success)时通知,
|
|
89
|
+
// success 路径不需要裁剪任何东西。
|
|
90
|
+
if (onFailedAttempt && outcome.status !== 'success') {
|
|
91
|
+
try {
|
|
92
|
+
const reflection = reflectOn(outcome.code);
|
|
93
|
+
onFailedAttempt({
|
|
94
|
+
fingerprint,
|
|
95
|
+
status: outcome.status,
|
|
96
|
+
code: outcome.code,
|
|
97
|
+
category: reflection.category,
|
|
98
|
+
attempts: attempt,
|
|
99
|
+
reflectionHint: reflection.hint,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// 钩子失败不影响主流程。
|
|
104
|
+
}
|
|
105
|
+
}
|
|
84
106
|
return {
|
|
85
107
|
...outcome,
|
|
86
108
|
durationMs: elapsed,
|
|
@@ -89,7 +111,15 @@ export async function executeWithToolRetry(capabilities, fingerprint, signal, ex
|
|
|
89
111
|
};
|
|
90
112
|
}
|
|
91
113
|
try {
|
|
92
|
-
|
|
114
|
+
const reflection = reflectOn(outcome.code);
|
|
115
|
+
onRetry?.({
|
|
116
|
+
attempt,
|
|
117
|
+
nextAttempt: attempt + 1,
|
|
118
|
+
waitMs,
|
|
119
|
+
code: outcome.code,
|
|
120
|
+
category: reflection.category,
|
|
121
|
+
reflectionHint: reflection.hint,
|
|
122
|
+
});
|
|
93
123
|
}
|
|
94
124
|
catch {
|
|
95
125
|
// Retry telemetry is best-effort and must never change tool execution.
|
|
@@ -97,6 +127,8 @@ export async function executeWithToolRetry(capabilities, fingerprint, signal, ex
|
|
|
97
127
|
await sleep(waitMs, signal);
|
|
98
128
|
retryDelayMs += waitMs;
|
|
99
129
|
}
|
|
130
|
+
// 不可达路径:循环要么 return,要么最后一次 attempt 也 shouldRetry=false(走 return)。
|
|
131
|
+
// 留 throw 是为了编译期穷尽性检查(TypeScript 期望函数末尾 return)。
|
|
100
132
|
throw new Error('unreachable tool retry state');
|
|
101
133
|
}
|
|
102
134
|
/** Test/session reset seam; production never needs to clear the bounded TTL map. */
|
package/dist/tools/validation.js
CHANGED
|
@@ -53,7 +53,7 @@ function formatErrors(errors) {
|
|
|
53
53
|
return `${location} ${error.message ?? error.keyword}`;
|
|
54
54
|
}).join('; ');
|
|
55
55
|
}
|
|
56
|
-
/** Validate
|
|
56
|
+
/** Validate after tool-local normalization; AJV itself never coerces or mutates arguments. */
|
|
57
57
|
export function validateToolArguments(tool, args) {
|
|
58
58
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
59
59
|
return {
|
|
@@ -62,6 +62,16 @@ export function validateToolArguments(tool, args) {
|
|
|
62
62
|
message: '参数根节点必须是 JSON object',
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
try {
|
|
66
|
+
tool.normalizeArguments?.(args);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return {
|
|
70
|
+
valid: false,
|
|
71
|
+
code: 'INVALID_ARGUMENTS',
|
|
72
|
+
message: `参数规范化失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
65
75
|
const compiled = compile(tool.parameters);
|
|
66
76
|
if (!compiled.valid) {
|
|
67
77
|
return {
|
package/dist/ui/batch.js
CHANGED
|
@@ -93,16 +93,19 @@ function buildSummaryLine(record, live = false) {
|
|
|
93
93
|
for (const [n, c] of counts)
|
|
94
94
|
parts.push(`${n} ${c}`);
|
|
95
95
|
const completed = entries.filter((e) => e.resultSummary || e.diffBlock || e.failed).length;
|
|
96
|
-
const
|
|
96
|
+
const failedCount = entries.filter((e) => e.failed).length;
|
|
97
97
|
// 工具本身完成就立即显示完成态,不等待整轮正文流完/onDone。
|
|
98
|
+
// 单项失败不代表整批失败:执行中优先展示进度;完成后区分部分失败与全部失败。
|
|
98
99
|
const finished = completed >= entries.length;
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
const allFailed = finished && failedCount === entries.length;
|
|
101
|
+
const partiallyFailed = finished && failedCount > 0 && !allFailed;
|
|
102
|
+
const symbol = !finished ? '◇' : allFailed ? '×' : partiallyFailed ? '!' : '●';
|
|
103
|
+
const color = !finished ? ui.accent : allFailed ? ui.red : partiallyFailed ? ui.yellow : ui.green;
|
|
104
|
+
const label = !finished
|
|
105
|
+
? t('agent.toolsRunning')
|
|
106
|
+
: allFailed
|
|
107
|
+
? t('agent.toolsFailed')
|
|
108
|
+
: t('agent.toolsComplete');
|
|
106
109
|
const progress = live && !finished ? ` ${completed}/${entries.length}` : ` ${entries.length}`;
|
|
107
110
|
const elapsedMs = record.finishedAt ? record.finishedAt - record.startedAt : 0;
|
|
108
111
|
const elapsed = record.finishedAt
|
package/dist/ui/layout.js
CHANGED
|
@@ -28,7 +28,7 @@ let turnStart = null; // RUNNING 态起点(Date.now());INPUT 态为 null。compo
|
|
|
28
28
|
let turnTimer = null; // 走时刷新计时器(独立于 spinner):流式期间 spinner 停转,由它续刷状态行。
|
|
29
29
|
// 运行态状态行 chip 心跳帧(♥/♡ 明灭)。turnTimer 每 tick 推进一帧,让状态行前导符在 agent
|
|
30
30
|
// 运行时跳动——agent 的 spinner 走内容区续写位(paintLiveAtCursor),不调 setStatus,
|
|
31
|
-
// 故状态行 chip 靠 turnTimer 独立驱动。INPUT 态 runningFrame=-1,composeStatus 退回静态
|
|
31
|
+
// 故状态行 chip 靠 turnTimer 独立驱动。INPUT 态 runningFrame=-1,composeStatus 退回静态 ●。
|
|
32
32
|
const RUNNING_FRAMES = ['♥', '♡'];
|
|
33
33
|
let runningFrame = -1;
|
|
34
34
|
// 运行态用户打字时暂停流式物理写:流式每个 token 要 cup 到 contentRow 写入,IME 候选窗逐光标移动跟踪会跟过去;
|
|
@@ -1245,10 +1245,10 @@ function twoColumn(leftStr, leftW, rightStr, rightW, cols) {
|
|
|
1245
1245
|
/** 上线之上那行(spinner 行):左段 = spinner 帧 + 状态文字 + 走时(全部左对齐,紧跟不分离)。
|
|
1246
1246
|
* 右段仅在滚动回看时显历史指示(右端对齐)。
|
|
1247
1247
|
* 示例:
|
|
1248
|
-
* INPUT:
|
|
1248
|
+
* INPUT: ● 空闲
|
|
1249
1249
|
* 思考中: ⠹ 思考中… 0.5s
|
|
1250
1250
|
* 运行心跳: ♥ 0.5s
|
|
1251
|
-
* 滚动回看:
|
|
1251
|
+
* 滚动回看: ● 空闲 历史 ↑3 (PgDn 回底) */
|
|
1252
1252
|
function composeSpinnerLine(status, cols) {
|
|
1253
1253
|
const spinning = mode === 'running' && runningFrame >= 0;
|
|
1254
1254
|
const hasSpinner = !!status.spinnerFrame;
|
|
@@ -1261,11 +1261,11 @@ function composeSpinnerLine(status, cols) {
|
|
|
1261
1261
|
let lead;
|
|
1262
1262
|
let leadW;
|
|
1263
1263
|
if (scrolled) {
|
|
1264
|
-
// 滚动回看:左段 = 符号(
|
|
1265
|
-
// 跟非回看的 INPUT/RUNNING
|
|
1264
|
+
// 滚动回看:左段 = 符号(● 或 心跳帧) + 状态名(灰,无走时);右段 = 历史指示。
|
|
1265
|
+
// 跟非回看的 INPUT/RUNNING 态保持一致——避免「● 留下、状态字蒸发」的视觉错觉。
|
|
1266
1266
|
const symbol = spinning
|
|
1267
1267
|
? `${ui.bold}${ui.accent}${RUNNING_FRAMES[runningFrame]}${ui.reset}`
|
|
1268
|
-
: `${ui.accent}
|
|
1268
|
+
: `${ui.accent}●${ui.reset}`;
|
|
1269
1269
|
lead = `${symbol} ${ui.dim}${status.status}${ui.reset}`;
|
|
1270
1270
|
leadW = 1 + 1 + displayWidth(status.status);
|
|
1271
1271
|
}
|
|
@@ -1282,8 +1282,8 @@ function composeSpinnerLine(status, cols) {
|
|
|
1282
1282
|
leadW = 1 + (elapsed ? 1 + displayWidth(elapsed) : 0);
|
|
1283
1283
|
}
|
|
1284
1284
|
else {
|
|
1285
|
-
// INPUT
|
|
1286
|
-
lead = `${ui.accent}
|
|
1285
|
+
// INPUT 态:● + 状态文字(无走时)
|
|
1286
|
+
lead = `${ui.accent}●${ui.reset} ${ui.dim}${status.status}${ui.reset}`;
|
|
1287
1287
|
leadW = 1 + 1 + displayWidth(status.status);
|
|
1288
1288
|
}
|
|
1289
1289
|
// ── 右段:仅滚动回看时显历史指示 ──
|
|
@@ -1378,7 +1378,7 @@ function composePlanLine(status, cols) {
|
|
|
1378
1378
|
/** 画状态行(plan 行 + spinner 行 + model 行,三行)。RUNNING 态 spinner 频繁调。
|
|
1379
1379
|
* 行号(footerH=6):
|
|
1380
1380
|
* plan 行 = contentBottom+1 (活跃 plan 时显 chip;无则空)
|
|
1381
|
-
* spinner 行 = contentBottom+2 (
|
|
1381
|
+
* spinner 行 = contentBottom+2 (● 空闲 / ⠹ 思考中… / etc)
|
|
1382
1382
|
* 上线 = contentBottom+3 (画在 paintInput)
|
|
1383
1383
|
* 输入行 = contentBottom+4
|
|
1384
1384
|
* 下线 = contentBottom+5
|
|
@@ -1832,7 +1832,7 @@ export function enterInputMode(status = t('repl.idle')) {
|
|
|
1832
1832
|
mode = 'input';
|
|
1833
1833
|
statusText = status;
|
|
1834
1834
|
spinnerFrame = undefined;
|
|
1835
|
-
runningFrame = -1; // 回 INPUT 态:停状态行 chip 旋转,composeStatus 退回静态
|
|
1835
|
+
runningFrame = -1; // 回 INPUT 态:停状态行 chip 旋转,composeStatus 退回静态 ●
|
|
1836
1836
|
turnStart = null; // 停走时
|
|
1837
1837
|
stopTurnTimer();
|
|
1838
1838
|
scrollLockUntil = 0; // 轮末:清轮首滚动锁,INPUT 态可自由滚动
|
package/dist/ui/render.js
CHANGED
|
@@ -261,7 +261,7 @@ function memoryValue(on) {
|
|
|
261
261
|
/** 横幅纯文本(带 ANSI 颜色,不写出)——供 TUI 经 contentWrite 写入内容区以跟踪续写位。
|
|
262
262
|
* 布局:大字 logo(4 行,块字符)左对齐,右侧并排放标题/信息(neofetch 风)。 */
|
|
263
263
|
export function bannerString(info) {
|
|
264
|
-
const title = `${ui.bold}${ui.accent}
|
|
264
|
+
const title = `${ui.bold}${ui.accent}● MoCode${ui.reset} ${ui.dim}v${VERSION}${ui.reset}`;
|
|
265
265
|
const labels = [t('banner.model'), t('banner.directory'), t('banner.memory')];
|
|
266
266
|
// 标签列按当前语言最长文本动态定宽,并至少留两个空格;中文保持原 6 列,英文扩至 11 列。
|
|
267
267
|
const labelWidth = Math.max(...labels.map(displayWidth)) + 2;
|