mocode-ai 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -40
- package/README.zh-CN.md +51 -41
- package/dist/agent/core.js +137 -447
- package/dist/agent/index.js +5 -24
- package/dist/agent/spawn.js +5 -5
- package/dist/agent/work-discipline.js +16 -70
- package/dist/config/index.js +87 -33
- package/dist/context/age-aware.js +18 -48
- package/dist/context/artifacts.js +19 -17
- package/dist/context/budget.js +27 -28
- package/dist/context/classifier.js +0 -1
- package/dist/context/encoders/index.js +4 -11
- package/dist/context/index.js +4 -7
- package/dist/context/lifecycle.js +115 -483
- package/dist/context/pipeline.js +8 -15
- package/dist/context/relevance.js +77 -55
- package/dist/host/stdio.js +0 -6
- package/dist/i18n/index.js +20 -6
- package/dist/index.js +11 -1
- package/dist/llm/index.js +82 -9
- package/dist/mcp/index.js +0 -1
- package/dist/repl/index.js +69 -15
- package/dist/runtime/browser-manager.js +299 -0
- package/dist/runtime/dev-server-manager.js +354 -0
- package/dist/runtime/shutdown.js +26 -0
- package/dist/session/compact.js +86 -102
- package/dist/session/index.js +0 -1
- package/dist/session/notes.js +107 -0
- package/dist/session/scheduler.js +88 -92
- package/dist/session/trace-metrics.js +5 -92
- package/dist/session/trace.js +1 -10
- package/dist/tools/builtins/browser.js +199 -0
- package/dist/tools/builtins/dev-server.js +99 -0
- package/dist/tools/builtins/index.js +33 -19
- package/dist/tools/builtins/plan-update.js +144 -0
- package/dist/tools/builtins/screenshot.js +173 -0
- package/dist/tools/builtins/view-image.js +49 -0
- package/dist/tools/constants.js +38 -8
- package/dist/tools/registry.js +5 -29
- package/dist/ui/batch.js +3 -0
- package/dist/ui/content.js +19 -16
- package/dist/ui/layout.js +120 -46
- package/dist/ui/render.js +11 -0
- package/package.json +2 -2
- package/dist/agent/middleware/checklist.js +0 -59
- package/dist/session/drop.d.ts +0 -19
- package/dist/session/drop.js +0 -93
- package/dist/tools/builtins/drop-context.d.ts +0 -18
- package/dist/tools/builtins/drop-context.js +0 -68
- package/dist/verification/diagnostics.js +0 -108
- package/dist/verification/fingerprint.js +0 -54
- package/dist/verification/index.js +0 -333
- package/dist/verification/postconditions.js +0 -98
- package/dist/verification/targeted-tests.js +0 -96
- package/dist/verification/types.js +0 -1
package/dist/agent/index.js
CHANGED
|
@@ -15,7 +15,6 @@ import { createPetHooks } from '../pet/state.js';
|
|
|
15
15
|
import { t } from '../i18n/index.js';
|
|
16
16
|
import { isToolErrorOutput } from '../tools/result.js';
|
|
17
17
|
import { appendCurrentSessionTraceEvent } from '../session/index.js';
|
|
18
|
-
import { buildActiveNotesPlanReminder } from '../session/notes-plan.js';
|
|
19
18
|
/** 当前 turn 的 batch id(runAgent 内闭包变量;一条 turn 一轮 tool batch 结束即清空)。 */
|
|
20
19
|
let currentBatchId = null;
|
|
21
20
|
let turnFileChanges = [];
|
|
@@ -118,9 +117,9 @@ function flushToolBatch(expandSingleEntry = false) {
|
|
|
118
117
|
batch.endBatch(id, layout);
|
|
119
118
|
if (expandSingleEntry)
|
|
120
119
|
batch.expandSingleEntryFully(id, layout);
|
|
121
|
-
//
|
|
122
|
-
// mutation
|
|
123
|
-
// diff
|
|
120
|
+
// 普通批:endBatch 留了 1 个 hasCurrent 空行,break 一次把它提交为分隔空行。
|
|
121
|
+
// mutation 自动展开:expandSingleEntryFully 自己补 separator (\n),这里不能再补 \n,
|
|
122
|
+
// 不然 diff 后面就会出现两条空白行。
|
|
124
123
|
if (!expandSingleEntry)
|
|
125
124
|
layout.contentWrite('\n');
|
|
126
125
|
}
|
|
@@ -202,6 +201,8 @@ onContextUpdate) {
|
|
|
202
201
|
},
|
|
203
202
|
onStepStart: () => spinner.start(t('agent.thinking')),
|
|
204
203
|
onChatDone: () => spinner.stop(),
|
|
204
|
+
// 流式实时用量 → 底栏 context 进度条左侧 chip;轮末由 repl 清空。
|
|
205
|
+
onLiveUsage: (u) => layout.setLiveUsage(u),
|
|
205
206
|
onTextEnd: () => {
|
|
206
207
|
if (lastChar && lastChar !== '\n') {
|
|
207
208
|
layout.contentWrite('\n');
|
|
@@ -240,24 +241,6 @@ onContextUpdate) {
|
|
|
240
241
|
flushToolBatch();
|
|
241
242
|
layout.contentWrite(`${ui.dim}${t('agent.aborted')}${ui.reset}\n`);
|
|
242
243
|
},
|
|
243
|
-
onValidationStart: (command) => {
|
|
244
|
-
flushToolBatch();
|
|
245
|
-
spinner.start(t('agent.validating', { command }));
|
|
246
|
-
},
|
|
247
|
-
onValidationResult: (validation) => {
|
|
248
|
-
spinner.stop();
|
|
249
|
-
const color = validation.status === 'passed'
|
|
250
|
-
? ui.green
|
|
251
|
-
: validation.status === 'failed'
|
|
252
|
-
? ui.red
|
|
253
|
-
: ui.yellow;
|
|
254
|
-
const command = validation.command ?? t('agent.validationNoCommand');
|
|
255
|
-
const detail = validation.status === 'skipped' && validation.skipReason
|
|
256
|
-
? `${validation.status}: ${validation.skipReason}`
|
|
257
|
-
: validation.status;
|
|
258
|
-
const symbol = validation.status === 'passed' ? '●' : validation.status === 'failed' ? '×' : '!';
|
|
259
|
-
layout.contentWrite(` ${color}${symbol}${ui.reset} ${t('agent.validationResult', { command, status: detail })}\n\n`);
|
|
260
|
-
},
|
|
261
244
|
onDone: (elapsedMs, usage) => {
|
|
262
245
|
flushToolBatch();
|
|
263
246
|
writeChangeOverview();
|
|
@@ -281,9 +264,7 @@ onContextUpdate) {
|
|
|
281
264
|
userInput,
|
|
282
265
|
signal,
|
|
283
266
|
onContextUpdate,
|
|
284
|
-
dynamicSystemSuffix: buildActiveNotesPlanReminder,
|
|
285
267
|
hooks: combinedHooks,
|
|
286
|
-
autoValidate: config.autoValidate,
|
|
287
268
|
onTraceEvent: appendCurrentSessionTraceEvent,
|
|
288
269
|
});
|
|
289
270
|
}
|
package/dist/agent/spawn.js
CHANGED
|
@@ -31,8 +31,8 @@ const SUBAGENT_ROLE = `## Sub-agent execution
|
|
|
31
31
|
You are executing one delegated sub-task with the same engineering standards and capabilities as mocode.
|
|
32
32
|
- Treat Task context as authoritative facts already established by the main agent; do not rediscover them without evidence they are stale.
|
|
33
33
|
- Focus on the delegated scope, but continue until it is genuinely complete. Do not stop to save tokens.
|
|
34
|
-
- Do not recursively call sub-agent. A write task runs in an isolated overlay; the coordinator merges
|
|
35
|
-
- Return concise findings, changes,
|
|
34
|
+
- Do not recursively call sub-agent. A write task runs in an isolated overlay; the coordinator merges it safely.
|
|
35
|
+
- Return concise findings, changes, checks you chose to run, and blockers to the coordinator.`;
|
|
36
36
|
/**
|
|
37
37
|
* 派生一个子 agent 执行独立子任务。
|
|
38
38
|
*
|
|
@@ -51,7 +51,7 @@ export async function spawnAgent(opts) {
|
|
|
51
51
|
summary: null,
|
|
52
52
|
completed: false,
|
|
53
53
|
transcript: 'Sub-agent execution is disabled. Enable it with /subagent on.',
|
|
54
|
-
status: 'failed', findings: [], readSet: [], changeSet: null,
|
|
54
|
+
status: 'failed', findings: [], readSet: [], changeSet: null,
|
|
55
55
|
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, reasoningTokens: 0 },
|
|
56
56
|
};
|
|
57
57
|
}
|
|
@@ -68,6 +68,8 @@ export async function spawnAgent(opts) {
|
|
|
68
68
|
const requested = opts.tools?.length ? new Set(opts.tools) : null;
|
|
69
69
|
const readOnly = new Set(['read_file', 'glob', 'grep', 'web_search', 'web_fetch', 'use_skill', 'memory_search', 'memory_list']);
|
|
70
70
|
toolsOverride = chatTools.filter((tool) => tool.function.name !== 'sub-agent' &&
|
|
71
|
+
// plan_update 直写主会话 notes.md(不走 overlay),子代理不应改动主计划——统一排除。
|
|
72
|
+
tool.function.name !== 'plan_update' &&
|
|
71
73
|
(!requested || requested.has(tool.function.name)) &&
|
|
72
74
|
(mode === 'write' || readOnly.has(tool.function.name)));
|
|
73
75
|
// 独立 history(子 agent 自己持有,不共享主对话)。
|
|
@@ -135,7 +137,6 @@ export async function spawnAgent(opts) {
|
|
|
135
137
|
maxSteps,
|
|
136
138
|
toolsOverride,
|
|
137
139
|
contextState: localContextState,
|
|
138
|
-
autoValidate: false,
|
|
139
140
|
onToolOutcome: (tool, args) => {
|
|
140
141
|
if (tool === 'read_file' && typeof args.path === 'string')
|
|
141
142
|
readSet.add(args.path);
|
|
@@ -173,7 +174,6 @@ export async function spawnAgent(opts) {
|
|
|
173
174
|
findings: result.finalText ? [result.finalText] : [],
|
|
174
175
|
readSet: [...readSet].sort(),
|
|
175
176
|
changeSet,
|
|
176
|
-
verification: null, // 主 Agent 在所有 coordinator merge 完成后统一验证
|
|
177
177
|
usage: {
|
|
178
178
|
promptTokens: result.usage?.promptTokens ?? 0,
|
|
179
179
|
completionTokens: result.usage?.completionTokens ?? 0,
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
//
|
|
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)负责。
|
|
1
|
+
// Lightweight, advisory working guidance. The agent decides how much discovery and
|
|
2
|
+
// validation each task needs; the framework does not enforce a completion gate.
|
|
16
3
|
/**
|
|
17
4
|
* 从 config.model 字符串里嗅探 model family。匹配规则尽量宽松,够用即可。
|
|
18
5
|
* 未来 AHE 闭环后可以换成 config.modelFamily 字段。
|
|
@@ -33,40 +20,19 @@ export function inferModelFamily(model) {
|
|
|
33
20
|
* 4 阶段核心纪律(英文)。4 个 model family 共用此文本,只在首句与标题
|
|
34
21
|
* 标签上做轻量变体。保持短小,详细的完成检查由动态 checklist 按需注入。
|
|
35
22
|
*/
|
|
36
|
-
const CORE_SECTION = `## Working discipline — coding tasks
|
|
23
|
+
const CORE_SECTION = `## Working discipline — coding tasks
|
|
37
24
|
|
|
38
|
-
|
|
25
|
+
Use your judgment to choose the shortest reliable path from the request to a useful result.
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
27
|
+
- Inspect only the code and context needed for the next decision.
|
|
28
|
+
- Make the smallest coherent change and avoid unrelated refactors.
|
|
29
|
+
- Decide whether validation is useful based on risk, scope, available commands, and the user's request. Validation is optional, not a completion gate.
|
|
30
|
+
- When validation is useful, choose the smallest relevant check yourself; do not run broad test/build suites by default.
|
|
31
|
+
- Re-read or rerun only when evidence is stale or the next edit depends on exact current content.
|
|
32
|
+
- On failure, diagnose before retrying; after repeated identical failures, change approach.
|
|
33
|
+
- Report honestly what you changed, what you checked, and anything left uncertain.
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
- Make the smallest coherent change; avoid unrelated refactors.
|
|
47
|
-
- Add or update a focused test when behavior changes and the project has an applicable test suite.
|
|
48
|
-
- Re-read only when a dependent edit needs fresh exact content or state may be stale.
|
|
49
|
-
|
|
50
|
-
### Phase 3 — Verify
|
|
51
|
-
- Run the smallest executable check that proves the requested behavior, then read its complete result.
|
|
52
|
-
- Compare evidence with the user's request, not merely with the diff.
|
|
53
|
-
|
|
54
|
-
### Phase 4 — Fix
|
|
55
|
-
- Diagnose the root cause, make a focused correction, and rerun the relevant check.
|
|
56
|
-
- After two identical failures, change the approach instead of repeating the same call.
|
|
57
|
-
|
|
58
|
-
**Hard rule (non-negotiable):** "I read the code and it looks right" is not a completion signal. Report the verification performed, or state clearly why it could not be run.
|
|
59
|
-
|
|
60
|
-
**Hard rule (non-negotiable):** Never invent file paths, APIs, config keys, flags, or behavior. Every claim about the codebase must trace to tool output in this conversation; explicitly label anything you have not verified as an assumption.`;
|
|
61
|
-
/**
|
|
62
|
-
* 把核心段适配到指定 model family:只替换首行(语序 / 强动词),段标题
|
|
63
|
-
* 保持原样。Phase 内容保持原样,4 份共享同一份结构化文本。
|
|
64
|
-
* 注意:不再往标题注入 "[model: X]" 标签——它对模型是无意义噪声,
|
|
65
|
-
* 还可能引发自我指涉,反而干扰遵从。
|
|
66
|
-
*/
|
|
67
|
-
function adapt(_model, opener) {
|
|
68
|
-
return CORE_SECTION.replace('Treat "verification" as a first-class part of the task, not an afterthought.', opener);
|
|
69
|
-
}
|
|
35
|
+
Never invent file paths, APIs, config keys, flags, or behavior. Distinguish repository evidence from assumptions.`;
|
|
70
36
|
/** ASK-01: only user-owned, high-impact choices should interrupt autonomous execution. */
|
|
71
37
|
const ASK_WHITELIST_SECTION = `## When to ask instead of guess
|
|
72
38
|
|
|
@@ -76,30 +42,10 @@ Call \`ask_human\` before coding only when repository evidence cannot resolve a
|
|
|
76
42
|
3. multiple reasonable options that materially change product behavior;
|
|
77
43
|
4. the request itself admits two or more materially different readings that lead to different deliverables (do not silently pick one and guess).
|
|
78
44
|
|
|
79
|
-
For naming
|
|
45
|
+
For naming and implementation details, follow repository precedent and choose the safest reversible default. Disclose any consequential assumption.
|
|
80
46
|
|
|
81
47
|
Budget: at most 2 \`ask_human\` calls per turn. Beyond that, use the safest reversible default and disclose it in the final reply.`;
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* ASK-01 段是固定英文,不参与 per-model 适配(避免 4 份变体维护成本)。
|
|
86
|
-
*/
|
|
87
|
-
export function buildWorkDisciplineSection(modelFamily) {
|
|
88
|
-
let section;
|
|
89
|
-
switch (modelFamily) {
|
|
90
|
-
case 'anthropic':
|
|
91
|
-
section = adapt('anthropic', 'Verification is a hard prerequisite for completion, not a courtesy.');
|
|
92
|
-
break;
|
|
93
|
-
case 'openai':
|
|
94
|
-
section = adapt('openai', 'Every coding task MUST complete these four phases in order. Skipping or merging phases is treated as a failure. For trivial or read-only requests, phases may collapse.');
|
|
95
|
-
break;
|
|
96
|
-
case 'qwen':
|
|
97
|
-
section = adapt('qwen', 'Verification is a hard prerequisite for completion; "I wrote the code" is not evidence the code works.');
|
|
98
|
-
break;
|
|
99
|
-
case 'other':
|
|
100
|
-
case undefined:
|
|
101
|
-
default:
|
|
102
|
-
section = CORE_SECTION;
|
|
103
|
-
}
|
|
104
|
-
return `${section}\n\n${ASK_WHITELIST_SECTION}`;
|
|
48
|
+
/** Advisory guidance shared by main and sub-agents. */
|
|
49
|
+
export function buildWorkDisciplineSection(_modelFamily) {
|
|
50
|
+
return `${CORE_SECTION}\n\n${ASK_WHITELIST_SECTION}`;
|
|
105
51
|
}
|
package/dist/config/index.js
CHANGED
|
@@ -2,8 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
|
-
import { getSandboxRoot } from '../sandbox/root.js';
|
|
6
5
|
import { getCurrentSessionId } from '../session/state.js';
|
|
6
|
+
import { getNotesFilePath } from '../session/notes.js';
|
|
7
7
|
import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
|
|
8
8
|
import { detectLanguage, setLanguage, t, } from '../i18n/index.js';
|
|
9
9
|
/**
|
|
@@ -41,6 +41,7 @@ export const languageFromShell = process.env.MOCODE_LANGUAGE !== undefined;
|
|
|
41
41
|
// 仿 themeFromShell 模式:shell export 的环境变量在 loadEnvFiles 中不被回填(优先级最高),
|
|
42
42
|
// 故 /model 写入 ~/.mocode/config 的同名键下次启动会被 shell 值覆盖——据此给 dim 警告。
|
|
43
43
|
const LLM_ENV_KEYS = ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS'];
|
|
44
|
+
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 256000;
|
|
44
45
|
const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
|
|
45
46
|
loadEnvFiles();
|
|
46
47
|
setLanguage(detectLanguage(process.env.MOCODE_LANGUAGE));
|
|
@@ -88,11 +89,8 @@ const PLATFORM_NOTE = (() => {
|
|
|
88
89
|
* 这样比纯目录列表更显眼,降低 agent 在长上下文里扫过去就忘了的概率。
|
|
89
90
|
*/
|
|
90
91
|
export function buildNotepadSection(sessionId = getCurrentSessionId()) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const root = getSandboxRoot() ?? process.cwd();
|
|
94
|
-
const p = path.join(root, '.mocode', 'sessions', sessionId, 'notes.md');
|
|
95
|
-
if (!fs.existsSync(p))
|
|
92
|
+
const p = getNotesFilePath(sessionId);
|
|
93
|
+
if (!p || !fs.existsSync(p))
|
|
96
94
|
return '';
|
|
97
95
|
try {
|
|
98
96
|
const content = fs.readFileSync(p, 'utf8').trim();
|
|
@@ -132,6 +130,55 @@ export function buildNotepadSection(sessionId = getCurrentSessionId()) {
|
|
|
132
130
|
return '';
|
|
133
131
|
}
|
|
134
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* 抽取 notes.md 中**唯一活跃**的 `## Plan:` 段原文(含标题行到下一个 `## ` 之前)。
|
|
135
|
+
* 用于 compact 后把计划重注入系统提示,避免 agent 因上下文压缩丢失执行计划。
|
|
136
|
+
* 已结算(`## Done:`)或无 plan 时返回 null。
|
|
137
|
+
*/
|
|
138
|
+
export function extractActivePlanSection(sessionId = getCurrentSessionId()) {
|
|
139
|
+
const p = getNotesFilePath(sessionId);
|
|
140
|
+
if (!p || !fs.existsSync(p))
|
|
141
|
+
return null;
|
|
142
|
+
try {
|
|
143
|
+
const normalized = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
|
|
144
|
+
const lines = normalized.split('\n');
|
|
145
|
+
const start = lines.findIndex((l) => /^## Plan:\s*.+$/.test(l));
|
|
146
|
+
if (start < 0)
|
|
147
|
+
return null;
|
|
148
|
+
const endOffset = lines.slice(start + 1).findIndex((l) => /^##\s/.test(l));
|
|
149
|
+
const end = endOffset < 0 ? lines.length : start + 1 + endOffset;
|
|
150
|
+
return lines.slice(start, end).join('\n').trimEnd();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** compact 重注入用的幂等标记:history[0] 中夹住活跃 plan 块,重复注入只替换不累积。 */
|
|
157
|
+
const ACTIVE_PLAN_MARKER = '\n\n<!-- mocode:active-plan -->\n';
|
|
158
|
+
/**
|
|
159
|
+
* 把活跃 `## Plan:` 段重注入系统提示(history[0])。compact 后调用:
|
|
160
|
+
* 若 notes.md 有活跃 plan,则覆盖旧标记块写入最新内容;若无,则清掉残留标记块。
|
|
161
|
+
* 直接改 history[0].content(compact 不破坏 index 0),幂等,返回是否改动。
|
|
162
|
+
*/
|
|
163
|
+
export function reinjectActivePlanIntoSystem(history) {
|
|
164
|
+
const sys = history[0];
|
|
165
|
+
if (!sys || sys.role !== 'system' || typeof sys.content !== 'string')
|
|
166
|
+
return false;
|
|
167
|
+
let content = sys.content;
|
|
168
|
+
const markerIdx = content.indexOf(ACTIVE_PLAN_MARKER);
|
|
169
|
+
if (markerIdx >= 0) {
|
|
170
|
+
content = content.slice(0, markerIdx).replace(/\s+$/, '');
|
|
171
|
+
}
|
|
172
|
+
const plan = extractActivePlanSection();
|
|
173
|
+
if (!plan) {
|
|
174
|
+
if (markerIdx < 0)
|
|
175
|
+
return false;
|
|
176
|
+
sys.content = content;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
sys.content = `${content}${ACTIVE_PLAN_MARKER}${plan}\n`;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
135
182
|
const SYSTEM_PROMPT_MEMORY_SECTION = `
|
|
136
183
|
## Memory (cross-session facts)
|
|
137
184
|
- The prompt may contain a title/summary index; retrieve details with memory_search or inspect all with memory_list.
|
|
@@ -195,7 +242,7 @@ ${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
|
195
242
|
|
|
196
243
|
## Workflow
|
|
197
244
|
- Use existing conversation and tool evidence before gathering more. Inspect only what supports the next decision; do not guess.
|
|
198
|
-
- Keep changes focused.
|
|
245
|
+
- Keep changes focused. Decide for yourself whether a check is worth running; prefer the smallest relevant check and avoid broad test/build suites unless the task or risk justifies them.
|
|
199
246
|
- Use web search only when freshness materially affects the answer.
|
|
200
247
|
${buildCodegraphSection()}
|
|
201
248
|
|
|
@@ -220,31 +267,30 @@ ${buildCodegraphSection()}
|
|
|
220
267
|
- **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
|
|
221
268
|
- 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.
|
|
222
269
|
${t('assistant.languageInstruction')}`;
|
|
223
|
-
// 动态段(置于末尾):memory 索引 + notepad
|
|
224
|
-
// 按需注入(#13)
|
|
225
|
-
// - "## Project context" 仅当 memorySection/notepadSection
|
|
226
|
-
// - notepad
|
|
227
|
-
// 无文件时连 marker 都不拼,既省 token 也让前缀缓存更稳。core 切片
|
|
228
|
-
// 回退到 MARKER_DYNAMIC_SECTION 或整段(见 buildMocodeCorePrompt)。
|
|
270
|
+
// 动态段(置于末尾):memory 索引 + notepad 索引 + notepad 使用说明。
|
|
271
|
+
// 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
|
|
272
|
+
// - "## Project context" 仅当 memorySection/notepadSection 非空(notepad 索引依赖 notes.md 存在);
|
|
273
|
+
// - notepad 使用说明**无条件**注入:否则会陷入"说明依赖 notes.md 存在 → 模型不知要建 → 文件永不存在"的鸡生蛋循环,功能对模型不可见。说明放在 prompt 末尾,不影响 staticBody 的前缀缓存。
|
|
229
274
|
const dynamicParts = [];
|
|
230
275
|
const ctxContent = `${memorySection}${notepadSection}`.trimEnd();
|
|
231
276
|
if (ctxContent) {
|
|
232
277
|
dynamicParts.push(`## Project context (dynamic reference)\n${ctxContent}`);
|
|
233
278
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
279
|
+
dynamicParts.push(`## Session Notepad (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
|
|
280
|
+
'Use this compact, persistent working surface for tasks with at least three steps or context-loss risk; skip it for simple work.\n\n' +
|
|
281
|
+
'Record and update the execution plan with the `plan_update` tool (preferred over editing checkboxes by hand); it keeps at most one active plan as a `## Plan:` section:\n' +
|
|
282
|
+
'```\n' +
|
|
283
|
+
'## Plan: <title>\n' +
|
|
284
|
+
'Goal: <outcome>\n' +
|
|
285
|
+
'### Steps\n' +
|
|
286
|
+
'- [ ] 1. <self-contained step: target file/symbol, the change, and how to verify>\n' +
|
|
287
|
+
'### Progress\n' +
|
|
288
|
+
'- <completed/total>\n' +
|
|
289
|
+
'```\n' +
|
|
290
|
+
'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. ' +
|
|
291
|
+
'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. ' +
|
|
292
|
+
'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. ' +
|
|
293
|
+
'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.');
|
|
248
294
|
return `${staticBody}\n\n${dynamicParts.join('\n\n')}`;
|
|
249
295
|
}
|
|
250
296
|
/** 静态主体结束 + 会话私有段起点标记,供 buildMocodeCorePrompt 稳健切片(#17)。 */
|
|
@@ -298,21 +344,20 @@ export const config = {
|
|
|
298
344
|
get systemPrompt() {
|
|
299
345
|
return buildBasePrompt();
|
|
300
346
|
},
|
|
301
|
-
contextWindowTokens: Number(process.env.CONTEXT_WINDOW_TOKENS) ||
|
|
302
|
-
compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
|
|
347
|
+
contextWindowTokens: Number(process.env.CONTEXT_WINDOW_TOKENS) || DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
303
348
|
includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
|
|
304
349
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE !== 'false',
|
|
350
|
+
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE === 'true',
|
|
351
|
+
contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE === 'true',
|
|
308
352
|
contextLifecycle: process.env.MOCODE_LIFECYCLE !== 'false',
|
|
309
353
|
contextBudget: process.env.MOCODE_BUDGET_SCHEDULER !== 'false',
|
|
310
|
-
autoReflect: process.env.AUTO_REFLECT
|
|
354
|
+
autoReflect: process.env.AUTO_REFLECT === 'true',
|
|
311
355
|
memoryEnabled: process.env.MEMORY_ENABLED === 'true',
|
|
312
356
|
reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
|
|
313
357
|
maxSteps: Number(process.env.MAX_STEPS) || 1000,
|
|
314
358
|
subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
|
|
315
359
|
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || Number(process.env.MAX_STEPS) || 1000,
|
|
360
|
+
frontendToolsEnabled: process.env.MOCODE_FRONTEND_TOOLS_ENABLED === 'true',
|
|
316
361
|
sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
|
|
317
362
|
searchApiKey: process.env.ANYSEARCH_API_KEY,
|
|
318
363
|
sandboxRoot: process.env.SANDBOX_ROOT || undefined,
|
|
@@ -361,6 +406,15 @@ export function updateSubAgentConfig(enabled) {
|
|
|
361
406
|
config.subAgentEnabled = enabled;
|
|
362
407
|
process.env.MOCODE_SUBAGENT_ENABLED = enabled ? 'true' : 'false';
|
|
363
408
|
}
|
|
409
|
+
/** 前端工具簇总开关;默认 false,关闭时 browser/dev_server/screenshot/view_image 不进入模型工具表。 */
|
|
410
|
+
export function isFrontendToolsEnabled() {
|
|
411
|
+
return config.frontendToolsEnabled;
|
|
412
|
+
}
|
|
413
|
+
/** 运行时切换前端工具簇;工具 schema 刷新与持久化由 REPL 调用方完成。 */
|
|
414
|
+
export function updateFrontendToolsConfig(enabled) {
|
|
415
|
+
config.frontendToolsEnabled = enabled;
|
|
416
|
+
process.env.MOCODE_FRONTEND_TOOLS_ENABLED = enabled ? 'true' : 'false';
|
|
417
|
+
}
|
|
364
418
|
/**
|
|
365
419
|
* 记忆子系统总开关:单一来源。/memory_switch、/memory_status、buildSystemPrompt、
|
|
366
420
|
* tools/builtins/index.ts、tools/constants.ts 的 plan-mode 列表都从这里查。
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
import { TOOL_OLD_AGE } from './budget.js';
|
|
1
|
+
// Pressure-only tool-result encoding coordinator.
|
|
2
|
+
// Normal pushes are raw apart from the per-result hard safety cap.
|
|
4
3
|
import { optimizeToolResult } from './pipeline.js';
|
|
5
4
|
import { canonicalizePath, extractPath, isToolResultSuccess, toText, } from './utils.js';
|
|
6
|
-
/**
|
|
7
|
-
* Tracks successful first reads and tool-result age without coupling encoders to
|
|
8
|
-
* lifecycle's mutable history indexes. All methods are fail-safe and idempotent.
|
|
9
|
-
*/
|
|
5
|
+
/** Rebuilds records from current history for each pressure pass. */
|
|
10
6
|
export class AgeAwareEncodingState {
|
|
11
7
|
pushOrdinal = 0;
|
|
12
8
|
records = new Map();
|
|
@@ -14,36 +10,16 @@ export class AgeAwareEncodingState {
|
|
|
14
10
|
constructor(history = []) {
|
|
15
11
|
this.rehydrate(history);
|
|
16
12
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.records.set(tc.id, {
|
|
24
|
-
toolCallId: tc.id,
|
|
25
|
-
toolName: tc.name,
|
|
26
|
-
argsRaw: tc.arguments,
|
|
27
|
-
pushOrdinal: this.pushOrdinal,
|
|
28
|
-
succeeded,
|
|
29
|
-
isFirstRead,
|
|
30
|
-
agedEncoded: false,
|
|
31
|
-
});
|
|
32
|
-
this.pushOrdinal++;
|
|
33
|
-
// Failed reads must not consume the "first successful read" privilege.
|
|
34
|
-
if (succeeded && path)
|
|
35
|
-
this.seenReadPaths.add(path);
|
|
36
|
-
return {
|
|
37
|
-
age: 0,
|
|
38
|
-
isCold: false,
|
|
39
|
-
isFirstRead,
|
|
40
|
-
phase: 'push',
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/** Re-encode eligible tool messages in the Cold prefix in place. */
|
|
44
|
-
sweep(history, hotBoundary) {
|
|
13
|
+
/**
|
|
14
|
+
* Pressure-only, progressive encoding of Cold logs and retrievable searches.
|
|
15
|
+
* It never touches code reads, skills, human decisions, or sub-agent output;
|
|
16
|
+
* repeated pressure passes may further reduce content only when strictly shorter.
|
|
17
|
+
*/
|
|
18
|
+
sweepPressure(history, hotBoundary) {
|
|
45
19
|
try {
|
|
20
|
+
const pressureEncodable = new Set(['run_command', 'grep', 'glob', 'web_search', 'web_fetch']);
|
|
46
21
|
const end = Math.min(Math.max(hotBoundary, 1), history.length);
|
|
22
|
+
let encodedCount = 0;
|
|
47
23
|
for (let idx = 1; idx < end; idx++) {
|
|
48
24
|
const message = history[idx];
|
|
49
25
|
if (message.role !== 'tool')
|
|
@@ -51,32 +27,27 @@ export class AgeAwareEncodingState {
|
|
|
51
27
|
const toolMessage = message;
|
|
52
28
|
const id = toolMessage.tool_call_id;
|
|
53
29
|
const record = id ? this.records.get(id) : undefined;
|
|
54
|
-
if (!record || !record.succeeded || record.
|
|
30
|
+
if (!record || !record.succeeded || !pressureEncodable.has(record.toolName))
|
|
55
31
|
continue;
|
|
56
32
|
const content = toText(toolMessage.content);
|
|
57
|
-
if (!content || content.startsWith('⌦['))
|
|
58
|
-
record.agedEncoded = true;
|
|
33
|
+
if (!content || content.startsWith('⌦['))
|
|
59
34
|
continue;
|
|
60
|
-
}
|
|
61
|
-
// Exclude the result's own push: immediately after insertion its age is 0.
|
|
62
35
|
const age = Math.max(0, this.pushOrdinal - record.pushOrdinal - 1);
|
|
63
|
-
if (age < TOOL_OLD_AGE)
|
|
64
|
-
continue;
|
|
65
36
|
const encoded = optimizeToolResult(record.toolName, content, record.argsRaw, {
|
|
66
37
|
age,
|
|
67
38
|
isCold: true,
|
|
68
39
|
isFirstRead: record.isFirstRead,
|
|
69
40
|
phase: 'sweep',
|
|
70
41
|
});
|
|
71
|
-
|
|
72
|
-
// representation that is equal-sized or larger.
|
|
73
|
-
if (encoded.length < content.length)
|
|
42
|
+
if (encoded.length < content.length) {
|
|
74
43
|
toolMessage.content = encoded;
|
|
75
|
-
|
|
44
|
+
encodedCount++;
|
|
45
|
+
}
|
|
76
46
|
}
|
|
47
|
+
return encodedCount;
|
|
77
48
|
}
|
|
78
49
|
catch {
|
|
79
|
-
|
|
50
|
+
return 0;
|
|
80
51
|
}
|
|
81
52
|
}
|
|
82
53
|
/** Rebuild stable state after resume or structural history compaction. */
|
|
@@ -119,7 +90,6 @@ export class AgeAwareEncodingState {
|
|
|
119
90
|
pushOrdinal: this.pushOrdinal,
|
|
120
91
|
succeeded,
|
|
121
92
|
isFirstRead,
|
|
122
|
-
agedEncoded: false,
|
|
123
93
|
});
|
|
124
94
|
this.pushOrdinal++;
|
|
125
95
|
if (succeeded && path)
|
|
@@ -132,10 +132,17 @@ export function recordArtifact(state, history, idx, output, succeeded) {
|
|
|
132
132
|
updateStats(state, stateFor(state));
|
|
133
133
|
}
|
|
134
134
|
function affected(artifact, changed) {
|
|
135
|
-
|
|
135
|
+
// '*' 依赖(无法解析出具体文件路径的诊断/搜索结果)不与任何具体写操作关联:
|
|
136
|
+
// 任何文件写入都会作废全部 '*' artifact,等于每次 mutation 都销毁
|
|
137
|
+
// git/测试/构建等历史证据,模型被迫反复 re-run,轮次爆炸。只失效路径明确命中的。
|
|
138
|
+
return artifact.dependencies.some((dependency) => dependency.path !== '*' && changed.has(dependency.path));
|
|
136
139
|
}
|
|
137
|
-
/**
|
|
138
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Mark precise, file-backed facts stale without changing the evidence in history.
|
|
142
|
+
* A stale result can still explain a later edit or failure; pressure compression is
|
|
143
|
+
* the only path allowed to replace its content with a compact marker.
|
|
144
|
+
*/
|
|
145
|
+
export function invalidateArtifacts(state, _history, changedFiles) {
|
|
139
146
|
const changed = new Set(changedFiles.map(canonicalizePath).filter((item) => !!item));
|
|
140
147
|
if (changed.size === 0)
|
|
141
148
|
return 0;
|
|
@@ -145,15 +152,6 @@ export function invalidateArtifacts(state, history, changedFiles) {
|
|
|
145
152
|
if (artifact.freshness !== 'fresh' || !affected(artifact, changed))
|
|
146
153
|
continue;
|
|
147
154
|
artifact.freshness = 'stale';
|
|
148
|
-
const message = history[artifact.messageIndex];
|
|
149
|
-
if (message?.role === 'tool') {
|
|
150
|
-
const original = toText(message.content);
|
|
151
|
-
const paths = artifact.dependencies.map((item) => item.path).join(', ');
|
|
152
|
-
const stub = `${STALE_PREFIX}${artifact.source.tool}] source=${artifact.id} dependencies=${paths} ` +
|
|
153
|
-
`invalidated-by=${[...changed].join(', ')}; re-run ${artifact.source.tool} before using this fact.`;
|
|
154
|
-
message.content = stub;
|
|
155
|
-
artifact.tokenCount = estimateTokens(stub);
|
|
156
|
-
}
|
|
157
155
|
count++;
|
|
158
156
|
}
|
|
159
157
|
updateStats(state, artifactState);
|
|
@@ -193,7 +191,7 @@ export function rehydrateArtifacts(state, history) {
|
|
|
193
191
|
}
|
|
194
192
|
updateStats(state, artifactState);
|
|
195
193
|
}
|
|
196
|
-
/** Compare captured dependency versions before each model step
|
|
194
|
+
/** Compare captured dependency versions before each model step and mark stale metadata only. */
|
|
197
195
|
export function refreshArtifactFreshness(state, history) {
|
|
198
196
|
const changed = new Set();
|
|
199
197
|
for (const artifact of stateFor(state).artifacts.values()) {
|
|
@@ -208,19 +206,23 @@ export function refreshArtifactFreshness(state, history) {
|
|
|
208
206
|
}
|
|
209
207
|
return changed.size > 0 ? invalidateArtifacts(state, history, [...changed]) : 0;
|
|
210
208
|
}
|
|
211
|
-
/**
|
|
212
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Pressure-only stage: replace stale evidence in the Cold prefix with a compact
|
|
211
|
+
* marker. Recent/current work stays intact, and normal mutation handling never
|
|
212
|
+
* calls this function.
|
|
213
|
+
*/
|
|
214
|
+
export function pruneStaleArtifacts(state, history, coldBoundary) {
|
|
213
215
|
const artifactState = stateFor(state);
|
|
214
216
|
let pruned = 0;
|
|
215
217
|
for (const artifact of artifactState.artifacts.values()) {
|
|
216
|
-
if (artifact.freshness !== 'stale')
|
|
218
|
+
if (artifact.freshness !== 'stale' || artifact.messageIndex >= coldBoundary)
|
|
217
219
|
continue;
|
|
218
220
|
const message = history[artifact.messageIndex];
|
|
219
221
|
if (message?.role !== 'tool')
|
|
220
222
|
continue;
|
|
221
223
|
const content = toText(message.content);
|
|
222
224
|
if (!content.startsWith(STALE_PREFIX)) {
|
|
223
|
-
message.content = `${STALE_PREFIX}${artifact.source.tool}] source=${artifact.id};
|
|
225
|
+
message.content = `${STALE_PREFIX}${artifact.source.tool}] source=${artifact.id}; source changed, so this content may be outdated.`;
|
|
224
226
|
artifact.tokenCount = estimateTokens(String(message.content));
|
|
225
227
|
pruned++;
|
|
226
228
|
}
|