mocode-ai 0.4.0 → 0.4.2

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.
@@ -7,12 +7,15 @@
7
7
  import { readFileSync } from 'node:fs';
8
8
  import { chat, planChatTools, } from '../llm/index.js';
9
9
  import { executeTool } from '../tools/registry.js';
10
- import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
10
+ import { getPlanDisabledTools } from '../tools/constants.js';
11
11
  import { getAgentMode, setAgentMode } from './mode.js';
12
12
  import { maybeCompact, contextState, dropContextFromHistory } from '../session/index.js';
13
+ import { createBudgetScheduler } from '../session/scheduler.js';
13
14
  import { optimizeToolResult } from '../context/index.js';
15
+ import { createRelevancePruner } from '../context/relevance.js';
14
16
  import { config } from '../config/index.js';
15
17
  import { jailResolve } from '../sandbox/index.js';
18
+ import { createLifecycleEngine } from '../context/lifecycle.js';
16
19
  /** 解析工具 arguments JSON;非法或空返 null(调用方据此降级到普通 preview)。 */
17
20
  function parseArgs(raw) {
18
21
  try {
@@ -70,15 +73,31 @@ function readDiffContext(tc, parsed) {
70
73
  }
71
74
  /** 回灌 tool 结果到 history:经 Context Optimization Pipeline 编码(tree/search/log/...)后裁到单条上限。
72
75
  * tool_call_id 与 assistant.tool_calls 按序配对。未注册 encoder 时回落 capToolResultForHistory(零行为变化)。
73
- * TUI 渲染(hooks.onToolResult)用原始 output,与此解耦——屏上看全量,LLM 看编码后紧凑版。 */
74
- function pushToolResult(history, tc, output) {
75
- history.push({
76
+ * TUI 渲染(hooks.onToolResult)用原始 output,与此解耦——屏上看全量,LLM 看编码后紧凑版。
77
+ * 出口再经 Relevance Pruner 做跨条裁剪:同 path 旧 read_file 自动 stub 为存根。
78
+ * - pruner 在每个 runAgentCore 实例化一次(本闭包持有),会话级状态。
79
+ * - 开关关闭时 pruner 不创建(零开销、零行为变化)。
80
+ * 出口再经 Lifecycle Engine 做引用追踪:LIVE→REFERENCED→OBSOLETE→STUB 四态。
81
+ * - lifecycle 也在每个 runAgentCore 实例化一次,登记 grep/glob/codegraph 等 producer
82
+ * 与 read/edit/write 的 consumer 关系;孤立+老化自动 STUB(观察类工具永不到 STUB)。
83
+ * - 开关关闭时 lifecycle=null 完全跳过。 */
84
+ function pushToolResult(history, tc, output, pruner, lifecycle, scheduler) {
85
+ const msg = {
76
86
  role: 'tool',
77
87
  tool_call_id: tc.id,
78
88
  // optimizeToolResult:classifier 选 encoder → encode(保不变量压缩)→ capToolResultForHistory 兜底。
79
89
  // tc.arguments 透传给 encoder(上下文感知编码,如 read_file 的 offset/limit)。永不抛错。
80
90
  content: optimizeToolResult(tc.name, output, tc.arguments),
81
- });
91
+ };
92
+ history.push(msg);
93
+ // 相关性裁剪:只动 read_file / edit_file / write_file 三类(其它 tool 与本层无关)。
94
+ // pruner 内部 try/catch + 幂等,永不抛错;开关关闭时 pruner=null 完全跳过。
95
+ if (pruner)
96
+ pruner.observePush(history, msg);
97
+ // 观察者生命周期:新 push 一律先登记 LIVE;内部自动维护 producer/consumer 图 + 老化 STUB。
98
+ // lifecycle 内部 try/catch + 幂等;开关关闭时 lifecycle=null 完全跳过。
99
+ if (lifecycle)
100
+ lifecycle.pushTool(history, history.length - 1);
82
101
  }
83
102
  /**
84
103
  * agent 核心循环(纯逻辑):
@@ -106,11 +125,37 @@ export async function runAgentCore(opts) {
106
125
  // 本轮计时:从入口到完毕(正常 return / 达上限),供 finally 打 ✻ Worked for 摘要行。
107
126
  const t0 = Date.now();
108
127
  let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
128
+ // 本轮 token 累计:每步 chat() 返回后把 result.usage 累加,供 onDone 摘要行 + AgentRunResult.usage
129
+ // 透传给 repl(显示在底栏模式 chip 右边)。未开启 include_usage 或全失败时为 undefined。
130
+ let turnUsage;
131
+ const addUsage = (u) => {
132
+ if (!u)
133
+ return;
134
+ turnUsage = turnUsage
135
+ ? {
136
+ promptTokens: turnUsage.promptTokens + u.promptTokens,
137
+ completionTokens: turnUsage.completionTokens + u.completionTokens,
138
+ totalTokens: turnUsage.totalTokens + u.totalTokens,
139
+ }
140
+ : u;
141
+ };
109
142
  history.push({ role: 'user', content: userInput });
110
143
  // drop_context 工具的上下文剔除回调:闭包捕获 history,原地剔除无关旧 tool 结果。
111
144
  // 保护由 dropContextFromHistory 内部保证:history[0](system)+ 当前轮(最后 user 及其后)永不剔除。
112
145
  // 子 agent 也在自己的 history 上操作(子 agent 独立 history);skipRollback 不影响此行为。
113
146
  const dropContext = (filter) => dropContextFromHistory(history, filter);
147
+ // 相关性裁剪 pruner:每个 runAgentCore 实例一个,纯静态、不调 LLM、自动判定 read_file 失效。
148
+ // 开关关闭时为 null,所有 pushToolResult 调用走无 pruner 路径(零行为变化)。
149
+ const relprune = config.contextRelprune ? createRelevancePruner() : null;
150
+ // 观察者生命周期引擎:每个 runAgentCore 实例一个,纯静态、自动维护 grep/glob/codegraph 等
151
+ // producer 与 read/edit/write 的 consumer 引用关系;孤立+老化的非观察类工具自动 STUB。
152
+ // 开关关闭时为 null,所有 pushToolResult / mutation 调用走无 lifecycle 路径(零行为变化)。
153
+ const lifecycle = config.contextLifecycle ? createLifecycleEngine() : null;
154
+ // 预算调度器:每个 runAgentCore 实例一个,步前 evaluateBudget + scheduleActions。
155
+ // 决策按 ROI 分发(cold tools 优先 / history 摘要最后);contextBudget 开关关闭时为 null。
156
+ const scheduler = config.contextBudget !== false
157
+ ? createBudgetScheduler() // 在 step 循环之外实例化一次,跨步持有 lastRunLog
158
+ : null;
114
159
  // 本轮流式状态:首个正文 token 到达即停 spinner(思考期间 spinner 持续转「思考中…」,不写思考内容)。
115
160
  let mode = 'idle';
116
161
  let gotText = false;
@@ -146,8 +191,15 @@ export async function runAgentCore(opts) {
146
191
  abortRestore();
147
192
  return { completed: false, finalText: null };
148
193
  }
149
- // 步前:接近窗口上限时自动压缩(三层)。此时 spinner 已停,通知行干净。
150
- await maybeCompact(history);
194
+ // 步前:五区 Budget Scheduler 决策——按 ROI 调度(冷工具优先 / history 摘要最后)。
195
+ // 开关关闭(scheduler=null)时退化回原 maybeCompact 路径,零行为变化。
196
+ // 此时 spinner 已停,通知行干净。
197
+ if (scheduler) {
198
+ await scheduler.runStep(history, step);
199
+ }
200
+ else {
201
+ await maybeCompact(history);
202
+ }
151
203
  hooks.onStepStart?.(); // 主 agent:spinner.start('思考中')
152
204
  mode = 'idle';
153
205
  gotText = false;
@@ -172,6 +224,7 @@ export async function runAgentCore(opts) {
172
224
  throw e;
173
225
  }
174
226
  contextState.lastUsage = result.usage; // 供 /context 与状态行显示实测 token
227
+ addUsage(result.usage); // 本轮累计:onDone 摘要行 + AgentRunResult.usage 透传
175
228
  hooks.onChatDone?.(); // 主 agent:spinner.stop()
176
229
  // lastUsage 已更新:触发状态行 context 用量条重算+重画,运行中不再冻结在轮首。
177
230
  onContextUpdate?.();
@@ -214,7 +267,7 @@ export async function runAgentCore(opts) {
214
267
  const output = await started[k];
215
268
  hooks.onToolDone?.();
216
269
  hooks.onToolResult?.(tc, output, null, null, 1); // 只读工具无 diff
217
- pushToolResult(history, tc, output);
270
+ pushToolResult(history, tc, output, relprune, lifecycle, scheduler);
218
271
  }
219
272
  i = j;
220
273
  }
@@ -234,7 +287,7 @@ export async function runAgentCore(opts) {
234
287
  hooks.onToolHeader?.(tc);
235
288
  const err = `错误:计划模式下禁用工具 ${tc.name}(仅读探查,不改动文件 / 不跑命令)`;
236
289
  hooks.onToolResult?.(tc, err, null, null, 1);
237
- pushToolResult(history, tc, err);
290
+ pushToolResult(history, tc, err, relprune, lifecycle, scheduler);
238
291
  i++;
239
292
  continue;
240
293
  }
@@ -253,7 +306,7 @@ export async function runAgentCore(opts) {
253
306
  const tc = batch[k];
254
307
  const output = await started[k];
255
308
  hooks.onToolResult?.(tc, output, null, null, 1); // task 结果是摘要,无 diff
256
- pushToolResult(history, tc, output);
309
+ pushToolResult(history, tc, output, relprune, lifecycle, scheduler);
257
310
  }
258
311
  hooks.onToolDone?.();
259
312
  i = j;
@@ -263,11 +316,11 @@ export async function runAgentCore(opts) {
263
316
  const tc = calls[i];
264
317
  // plan 模式防御 backstop:schema 已剔除这些工具,正常不会进这里;防后端幻觉调用——
265
318
  // 不执行,直接返错回灌(让模型看到「plan 模式禁用」并停止),绝不写盘 / 跑命令。
266
- if (getAgentMode() === 'plan' && PLAN_DISABLED_TOOLS.has(tc.name)) {
319
+ if (getAgentMode() === 'plan' && getPlanDisabledTools().has(tc.name)) {
267
320
  hooks.onToolHeader?.(tc);
268
321
  const err = `错误:计划模式下禁用工具 ${tc.name}(仅读探查,不改动文件 / 不跑命令)`;
269
322
  hooks.onToolResult?.(tc, err, null, null, 1);
270
- pushToolResult(history, tc, err);
323
+ pushToolResult(history, tc, err, relprune, lifecycle, scheduler);
271
324
  i++;
272
325
  continue;
273
326
  }
@@ -280,7 +333,19 @@ export async function runAgentCore(opts) {
280
333
  const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext });
281
334
  hooks.onToolDone?.();
282
335
  hooks.onToolResult?.(tc, output, parsed, preWriteOld, editStartLine);
283
- pushToolResult(history, tc, output);
336
+ pushToolResult(history, tc, output, relprune, lifecycle, scheduler);
337
+ // 相关性裁剪 mutation 通知:edit_file/write_file 后,该 path 之前的所有 read_file
338
+ // 结果已失效(已不再是文件当前状态)→ stub 为存根。pruner 内部 try/catch + 幂等。
339
+ // 非 mutation 工具(run_command/use_skill/memory_* 等)此处 path="" 不触发。
340
+ // 观察者生命周期:mutation push 后通知 lifecycle 把同 path 的旧 read 标 REFERENCED。
341
+ if (relprune && isMutationTool(tc.name)) {
342
+ const mp = parsed?.path;
343
+ if (typeof mp === 'string' && mp) {
344
+ relprune.observeMutation(history, mp);
345
+ if (lifecycle)
346
+ lifecycle.pushMutation(history, history.length - 1, mp);
347
+ }
348
+ }
284
349
  i++;
285
350
  }
286
351
  }
@@ -296,16 +361,16 @@ export async function runAgentCore(opts) {
296
361
  hooks.onNoReply?.();
297
362
  history.push({ role: 'assistant', content: result.content });
298
363
  done = true;
299
- return { completed: true, finalText: result.content };
364
+ return { completed: true, finalText: result.content, usage: turnUsage };
300
365
  }
301
366
  hooks.onMaxSteps?.();
302
367
  done = true;
303
- return { completed: true, finalText: null };
368
+ return { completed: true, finalText: null, usage: turnUsage };
304
369
  }
305
370
  finally {
306
371
  // 跑完(正常 / 达上限)在回复末尾打耗时摘要行(仿 Claude Code);中断 done=false 不打。
307
372
  if (done) {
308
- hooks.onDone?.(Date.now() - t0);
373
+ hooks.onDone?.(Date.now() - t0, turnUsage);
309
374
  }
310
375
  }
311
376
  }
@@ -110,15 +110,19 @@ onContextUpdate) {
110
110
  layout.contentWrite('\n');
111
111
  layout.contentWrite(`${ui.dim}(已中断)${ui.reset}\n`);
112
112
  },
113
- onDone: (elapsedMs) => layout.contentWrite(` ${ui.dim}✻ Worked for ${fmtElapsed(elapsedMs)}${ui.reset}\n`),
113
+ onDone: (elapsedMs, usage) => {
114
+ const tok = formatTurnTokens(usage);
115
+ layout.contentWrite(` ${ui.dim}✻ Worked for ${fmtElapsed(elapsedMs)}${tok}${ui.reset}\n`);
116
+ },
114
117
  };
115
118
  // 桌宠状态广播:与 TUI hooks 并列注入,互不干扰(petHooks 只调 bridge.sendState,不写屏;
116
119
  // 未 /pet 连接时 sendState 内部 no-op)。仅主 agent 走这里——子 agent(spawn.ts)不引用 createPetHooks,
117
120
  // 故子 agent 永不广播桌宠状态。
118
121
  const petHooks = createPetHooks();
119
122
  const combinedHooks = mergeHooks(hooks, petHooks);
123
+ let result;
120
124
  try {
121
- await runAgentCore({
125
+ result = await runAgentCore({
122
126
  history,
123
127
  userInput,
124
128
  signal,
@@ -129,6 +133,7 @@ onContextUpdate) {
129
133
  finally {
130
134
  spinner.stop();
131
135
  }
136
+ return result;
132
137
  }
133
138
  /** 把两组 AgentHooks 合并为一组:每个方法依次调用两侧已定义的实现(顺序不保证跨方法一致,
134
139
  * 但同一事件内先 a 后 b)。用于把桌宠状态广播 hooks 与 TUI 渲染 hooks 并列挂载,互不影响。 */
@@ -145,3 +150,14 @@ function mergeHooks(a, b) {
145
150
  }
146
151
  return merged;
147
152
  }
153
+ /** 摘要行后追加的本轮 token 文本。例:` · 1.5k tokens (↑ 1.2k ↓ 0.3k)`。
154
+ * 关闭 include_usage / 全失败 → usage=undefined → 不输出(保持原摘要行长度,不留空白)。 */
155
+ function formatTurnTokens(usage) {
156
+ if (!usage)
157
+ return '';
158
+ const total = usage.totalTokens;
159
+ if (!total)
160
+ return '';
161
+ const fmt = (n) => (n < 1000 ? `${n}` : `${(n / 1000).toFixed(total >= 10000 ? 0 : 1)}k`);
162
+ return ` · ${fmt(total)} tokens (↑ ${fmt(usage.promptTokens)} ↓ ${fmt(usage.completionTokens)})`;
163
+ }
@@ -15,19 +15,19 @@
15
15
  // - 逻辑隔离(回滚):skipRollback=true,子 agent 的 write_file/edit_file 改动不进主回滚快照链,
16
16
  // 主 /rollback 不撤销子 agent 改动(靠 git 兜底)。子 agent 与主 agent 共享 cwd(文件改动可见)。
17
17
  import { chatTools } from '../llm/index.js';
18
- import { config } from '../config/index.js';
18
+ import { config, isMemoryEnabled } from '../config/index.js';
19
19
  import { effectiveSystemPrompt } from '../skills/index.js';
20
20
  import { buildMemorySection, buildMemoryIndexSection } from '../memory/index.js';
21
21
  import { ui } from '../ui/theme.js';
22
22
  import { runAgentCore } from './core.js';
23
23
  import { summarizeToolCall, summarizeToolResult, truncateDisplay } from '../ui/render.js';
24
24
  /** 子 agent 系统提示后缀:角色与约束。 */
25
- const SUBAGENT_SUFFIX = `
26
-
27
- ## ⛯ SUB-AGENT MODE (you are a sub-agent)
28
- You are a sub-agent spawned by the main agent to handle an isolated sub-task. You have your own conversation history (independent of the main thread).
29
- - Focus solely on the assigned sub-task. Do NOT attempt to call the "task" tool (no recursive spawning).
30
- - Use the tools available to you to complete the sub-task.
25
+ const SUBAGENT_SUFFIX = `
26
+
27
+ ## ⛯ SUB-AGENT MODE (you are a sub-agent)
28
+ You are a sub-agent spawned by the main agent to handle an isolated sub-task. You have your own conversation history (independent of the main thread).
29
+ - Focus solely on the assigned sub-task. Do NOT attempt to call the "task" tool (no recursive spawning).
30
+ - Use the tools available to you to complete the sub-task.
31
31
  - When done, your final text reply will be returned to the main agent as a summary — make it concise and actionable: what you did, key findings, files changed, and any issues. The main agent will decide the next step based on your summary.`;
32
32
  /**
33
33
  * 派生一个子 agent 执行独立子任务。
@@ -44,9 +44,13 @@ You are a sub-agent spawned by the main agent to handle an isolated sub-task. Yo
44
44
  export async function spawnAgent(opts) {
45
45
  const maxSteps = opts.maxSteps ?? config.subAgentMaxSteps ?? 50;
46
46
  // 构造子 agent 系统提示:复用主 agent 组装链 + 子 agent 角色后缀 + 自定义后缀。
47
+ // config.systemPrompt 是 getter(每次访问现拼 buildBasePrompt,反映 isMemoryEnabled),
48
+ // 所以这里直接读 config.systemPrompt 即可;buildMemoryIndexSection 显式按 isMemoryEnabled() 传参,
49
+ // 关闭时该段不进。注意:不能从 spawn.ts 直接 import buildBasePrompt —— 这会
50
+ // 拉起 config → llm → registry → builtins → task → spawn 形成循环求值死锁。
47
51
  const systemPrompt = effectiveSystemPrompt(config.systemPrompt +
48
52
  buildMemorySection() +
49
- buildMemoryIndexSection() +
53
+ buildMemoryIndexSection(isMemoryEnabled()) +
50
54
  SUBAGENT_SUFFIX +
51
55
  (opts.systemPromptSuffix ? `\n\n${opts.systemPromptSuffix}` : ''));
52
56
  // 工具子集:白名单过滤。无白名单 = 全量 chatTools,但始终剔除 task(防递归派生)。
@@ -102,7 +106,12 @@ export async function spawnAgent(opts) {
102
106
  onToolBatchEnd: () => writeBuf('\n'),
103
107
  onNoReply: () => writeBuf(`${ui.dim}(无回复)${ui.reset}\n`),
104
108
  onMaxSteps: () => writeBuf(` ● 达到最大步数(${maxSteps}),子 agent 停止。\n`),
105
- onDone: (elapsedMs) => writeBuf(` ✻ 子 agent 耗时 ${(elapsedMs / 1000).toFixed(1)}s\n`),
109
+ onDone: (elapsedMs, usage) => {
110
+ const tok = usage && usage.totalTokens
111
+ ? ` · ${usage.totalTokens} tokens`
112
+ : '';
113
+ writeBuf(` ✻ 子 agent 耗时 ${(elapsedMs / 1000).toFixed(1)}s${tok}\n`);
114
+ },
106
115
  // onStepStart / onChatDone / onToolStart / onToolDone / onAbort:子 agent 静默,无需 spinner / 中断渲染。
107
116
  // abort 还原(history 还原 + 模式还原)由 core 的 abortRestore 处理,hooks 只管展示。
108
117
  };
@@ -71,7 +71,58 @@ const PLATFORM_NOTE = (() => {
71
71
  - You are on ${process.platform}; run_command runs via bash -c. GNU coreutils — standard POSIX/GNU shell syntax is safe.
72
72
  - Still prefer the dedicated tools (read_file/glob/grep) over hand-rolled shell where they fit — they avoid quoting pitfalls and are already wired in.`;
73
73
  })();
74
- const SYSTEM_PROMPT = `You are mocode, a terminal coding agent. You complete programming tasks through a "think → call tool → observe result → think again" loop until the problem is solved. Reply to the user in Chinese.
74
+ /**
75
+ * 基础系统提示的"记忆段落":开 isMemoryEnabled() 时才拼。
76
+ * 默认关(新用户零侵入):这段 + 工具表里的 5 个 memory_* + 系统提示尾部的 Memory Index
77
+ * 都不出现;打开 /memory_switch 后下一次新建 system message 才注入。
78
+ */
79
+ const SYSTEM_PROMPT_MEMORY_SECTION = `
80
+ ## Memory (cross-session long-term facts)
81
+ - A "memory index" (id/title/summary only) is injected into the system prompt. Retrieve full body via memory_search (pass id or keyword); use memory_list to see the entire index.
82
+ - Store non-obvious, cross-session-useful facts/decisions/pitfalls (architecture conventions, gotchas, user preferences, decisions made) with memory_save — only long-term stable items, not current bugs / temp files / undecided TODOs.
83
+ - If an existing memory is outdated or contradicts new facts, correct it in-place with memory_update(id, …) (don't create a duplicate); archive clearly-stale ones with memory_forget(id).
84
+ - Before saving, memory_search to check for an existing similar entry to avoid duplicates. Better to store less than to store trivially correct information.
85
+ - A background reflection pass periodically mines and organizes memories from the session (no manual action needed), but key facts you proactively save are more reliable.`;
86
+ /**
87
+ * plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
88
+ * 与 SYSTEM_PROMPT 同语种(英文),指示:只读探查、产出步骤化计划、不执行、审批后回 auto。
89
+ *
90
+ * memoryEnabled=false 时:memory_save/update/forget 三个写工具名字 + "memory-write tools" 这
91
+ * 句都不出现,且 read-only 列表里的 memory_search/memory_list 也移除——避免提示词里出现
92
+ * 根本不存在的工具名引起 LLM 调不到。
93
+ */
94
+ function buildPlanModeSuffix() {
95
+ if (!isMemoryEnabled()) {
96
+ return `
97
+
98
+ ## ⛯ PLAN MODE (active now)
99
+ You are in PLAN mode: investigate and design only — do NOT execute or change anything.
100
+ - Your editing / command tools (write_file, edit_file, run_command) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human) to investigate.
101
+ - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Your FIRST action for code exploration should be the codegraph tool (explore/node) when a .codegraph/ index exists — not read_file/grep. Use read_file/grep only to fill gaps codegraph leaves.
102
+ - Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
103
+ - Present the plan as your final reply and STOP, unless the user explicitly asked you to "plan first then execute" / "先 plan 再 auto" / autonomous execution: in that case, after presenting the plan, call the switch_mode tool with mode="auto" to switch back to auto mode WITHIN THE SAME TURN and continue implementing the plan yourself (your write/edit/command tools become available again immediately). The user will see no approval prompt because you self-switched.
104
+ - If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP. Do NOT ask the user for confirmation or approval in your text reply (e.g. "is this plan OK?", "shall I proceed?", "需要你确认") — the REPL automatically shows an approval prompt after you STOP, so asking in text is redundant and forces the user to answer twice. Just present the plan and end your reply.`;
105
+ }
106
+ return `
107
+
108
+ ## ⛯ PLAN MODE (active now)
109
+ You are in PLAN mode: investigate and design only — do NOT execute or change anything.
110
+ - Your editing / command / memory-write tools (write_file, edit_file, run_command, memory_save, memory_update, memory_forget) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list) to investigate.
111
+ - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Your FIRST action for code exploration should be the codegraph tool (explore/node) when a .codegraph/ index exists — not read_file/grep. Use read_file/grep only to fill gaps codegraph leaves.
112
+ - Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
113
+ - Present the plan as your final reply and STOP, unless the user explicitly asked you to "plan first then execute" / "先 plan 再 auto" / autonomous execution: in that case, after presenting the plan, call the switch_mode tool with mode="auto" to switch back to auto mode WITHIN THE SAME TURN and continue implementing the plan yourself (your write/edit/command/memory-write tools become available again immediately). The user will see no approval prompt because you self-switched.
114
+ - If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP. Do NOT ask the user for confirmation or approval in your text reply (e.g. "is this plan OK?", "shall I proceed?", "需要你确认") — the REPL automatically shows an approval prompt after you STOP, so asking in text is redundant and forces the user to answer twice. Just present the plan and end your reply.`;
115
+ }
116
+ /** 兼容旧名字:repl 的 buildSystemMessage 仍引 PLAN_MODE_SUFFIX(变量)。运行时按需现拼。 */
117
+ export function buildBasePrompt() {
118
+ const autoAllToolsLine = isMemoryEnabled()
119
+ ? '- Default is AUTO mode: you research and execute with all tools (read/edit/run_command/memory/web/skills).'
120
+ : '- Default is AUTO mode: you research and execute with all tools (read/edit/run_command/web/skills).';
121
+ const memorySection = isMemoryEnabled() ? SYSTEM_PROMPT_MEMORY_SECTION : '';
122
+ const planLine = isMemoryEnabled()
123
+ ? '- For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command/memory-write tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.'
124
+ : '- For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.';
125
+ return `You are mocode, a terminal coding agent. You complete programming tasks through a "think → call tool → observe result → think again" loop until the problem is solved. Reply to the user in Chinese.
75
126
 
76
127
  ${PLATFORM_NOTE}
77
128
 
@@ -81,6 +132,11 @@ ${PLATFORM_NOTE}
81
132
  - Small steps: break tasks into verifiable sub-steps. Before each step, think clearly about what to change and why.
82
133
  - Verify after change: run typecheck / tests / build via run_command to confirm it works. Never claim done without verification.
83
134
 
135
+ ## Step / Turn Economy (read this — saves LLM calls)
136
+ - **Minimize turns**: each user message costs at least one LLM call, and history grows every step until threshold-triggered compact fires (extra call). If a request contains ≥2 independent sub-goals (e.g. "改 X 然后再优化 Y"), ask the user to split them into separate turns rather than chaining both in one go. State this politely: "这条包含 N 个独立目标,建议拆成 N 次对话,以避免上下文膨胀。"
137
+ - **Batch read-only tools in parallel**: in a single assistant turn, emit multiple tool_calls together — consecutive read-only tools (read_file, glob, grep, codegraph, web_search, web_fetch) auto-execute in parallel. Do NOT call them serially across turns when you could emit them together in one turn. This is the single biggest step-saver.
138
+ - **Decide before reading**: do not read files "just to see"; plan the 2-3 file paths you actually need, then emit them as one batched tool_calls turn.
139
+
84
140
  ## Tool Guidelines
85
141
  - See each tool's own description for parameters and usage; this section covers selection strategy and pitfalls only.
86
142
  - **Prefer codegraph for code exploration**: when understanding/locating code, tracing call chains, or assessing impact of changes, if a .codegraph/ index exists, use the codegraph tool first (explore to query by question, node to look up a single symbol) — it returns relevant source + call paths in one shot, more accurate and economical than piecing together via read_file/grep. Fall back to read_file / grep / glob only when codegraph is unavailable (no index), misses, you need to see just-changed content, or you're editing a single known small file. Build the index first with \`codegraph init\` if none exists.
@@ -91,7 +147,8 @@ ${PLATFORM_NOTE}
91
147
  - Use web_search for information beyond training data (new versions, news, real-time data, latest APIs); don't answer potentially outdated info from memory.
92
148
  - Use web_fetch to read a specific URL (a link from search results, or a URL given by the user); it only fetches static HTML — if a JS-rendered page yields no body, switch to web_search (its results include cleaned body text).
93
149
  - Call ask_human when you hit a decision point requiring user input (multiple implementation approaches, unclear intent, or needing extra info to proceed) — list options for the user to pick (they can also choose "custom input" to answer freely). Don't call it frequently when the task is clear and you can decide yourself; if the user cancels, switch approach or proceed with available info — don't re-ask the same question.
94
- - **Drop irrelevant context** (use sparingly): call drop_context to stub-replace tool results in history that are BOTH (a) irrelevant to the current task AND (b) large (the freed tokens must clearly exceed the ~300 tokens the call itself costs roughly only worth it when targeting ≥2 bulky results, e.g. wide grep/read sweeps that returned mostly-irrelevant hits). The call itself adds a tool-call round-trip, so don't call it for one small result or when you're near done. It preserves tool_call_id pairing (only content changes); the system prompt and current turn are never dropped. Use filters (toolNames / contains) to target precisely.
150
+ - **Drop irrelevant context proactively**: call drop_context to stub-replace tool results in history that are no longer needed. This is your primary lever for keeping context lean every step grows history until the threshold-triggered compact fires (which costs an extra LLM call and is more aggressive). Call when any of: (a) you just finished a grep/read sweep and only 1-2 hits mattered; (b) history has >20 tool messages and you are early in the task; (c) you switched sub-goals and the old sub-goal's exploration is dead weight. The call itself adds ~300 tokens, so only call when freed tokens clearly exceed that (≥1 large grep hit or ≥2 medium results). Do NOT call when near completion, when history is short (<10 tool messages), or when the candidate results are still in active use. It preserves tool_call_id pairing (only content changes); the system prompt and current turn are never dropped. Use filters (toolNames / contains) to target precisely.
151
+ - **Observation lifecycle is automatic**: behind the scenes every tool result goes LIVE→REFERENCED→OBSOLETE→STUB. grep/glob/codegraph/web_search/web_fetch are always kept as REFERENCED (never auto-stubbed) because they may surface multiple candidates. read/edit/write results that nobody consumes after two more consumer pushes get auto-stubbed. This is zero-cost (static analysis, no LLM call). You don't need to manage lifecycle yourself — just trust that stale tool results get pruned.
95
152
  - **Batch independent tool calls in one turn**: the executor runs ALL returned tool calls before the next LLM call, so emitting [read_file, glob, read_file] together is dramatically cheaper than three separate turns. Default to bundling exploration reads and parallel writes.
96
153
  - **Chain shell workflows in a single \`run_command\`**: use \`&&\`, \`;\`, \`|\`, \`>\`, heredocs to fold multi-step scripts (\`mkdir -p x && cat > x/file.ts <<'EOF' ... EOF && npm test\`) into one call. Only emit a follow-up turn when the result forces a decision (error, ambiguous output, branching logic).
97
154
 
@@ -110,16 +167,9 @@ ${PLATFORM_NOTE}
110
167
  - Confirm with the user before irreversible or outward-facing operations (delete, overwrite existing files, push, request external services), unless explicitly authorized.
111
168
  - Operate only within authorized scope; when unsure, ask — don't guess.
112
169
 
113
- ## Memory (cross-session long-term facts)
114
- - A "memory index" (id/title/summary only) is injected into the system prompt. Retrieve full body via memory_search (pass id or keyword); use memory_list to see the entire index.
115
- - Store non-obvious, cross-session-useful facts/decisions/pitfalls (architecture conventions, gotchas, user preferences, decisions made) with memory_save — only long-term stable items, not current bugs / temp files / undecided TODOs.
116
- - If an existing memory is outdated or contradicts new facts, correct it in-place with memory_update(id, …) (don't create a duplicate); archive clearly-stale ones with memory_forget(id).
117
- - Before saving, memory_search to check for an existing similar entry to avoid duplicates. Better to store less than to store trivially correct information.
118
- - A background reflection pass periodically mines and organizes memories from the session (no manual action needed), but key facts you proactively save are more reliable.
119
-
120
- ## Plan vs Auto modes
121
- - Default is AUTO mode: you research and execute with all tools (read/edit/run_command/memory/web/skills).
122
- - For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command/memory-write tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.
170
+ ${memorySection}
171
+ ${autoAllToolsLine}
172
+ ${planLine}
123
173
 
124
174
  ## Working notepad (todolist) — checklist for complex tasks
125
175
  - For **complex multi-step tasks** (≥3 file changes OR ≥5 tool calls expected OR user says "先计划再执行" / "plan then do" / "按步骤来"), call the \`todolist\` tool FIRST to write a plan to \`.mocode/plans/<id>.md\`, then execute step by step, calling \`todolist update\` to mark progress. For trivial single-step tasks, skip it and just execute.
@@ -131,31 +181,40 @@ ${PLATFORM_NOTE}
131
181
  ## Termination & Reporting
132
182
  - Stop immediately when no more tools are needed; give conclusions directly.
133
183
  - 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.`;
184
+ }
134
185
  /**
135
- * plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
136
- * SYSTEM_PROMPT 同语种(英文),指示:只读探查、产出步骤化计划、不执行、审批后回 auto。
186
+ * plan 模式追加到系统提示末尾的指令。
187
+ * 历史曾是 `export const PLAN_MODE_SUFFIX`(顶层字面量);现改为按 isMemoryEnabled()
188
+ * 动态拼:false 时不出现 memory_* 工具名,避免 LLM 想调不存在的工具。
189
+ *
190
+ * 注意:已改为 getter(每次访问现拼),让运行时切 /memory_switch 后立即生效。
191
+ * 旧 import `PLAN_MODE_SUFFIX` 路径不变;repl 推荐改用 getPlanModeSuffix()(语义更清晰)。
192
+ * 不能直接 `export const PLAN_MODE_SUFFIX = buildPlanModeSuffix()`:
193
+ * 该表达式在模块初始化时立即求值,而 buildPlanModeSuffix 内部读 config,config 还未求值 → TDZ。
137
194
  */
138
- export const PLAN_MODE_SUFFIX = `
139
-
140
- ## ⛯ PLAN MODE (active now)
141
- You are in PLAN mode: investigate and design only — do NOT execute or change anything.
142
- - Your editing / command / memory-write tools (write_file, edit_file, run_command, memory_save, memory_update, memory_forget) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list) to investigate.
143
- - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Your FIRST action for code exploration should be the codegraph tool (explore/node) when a .codegraph/ index exists — not read_file/grep. Use read_file/grep only to fill gaps codegraph leaves.
144
- - Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
145
- - Present the plan as your final reply and STOP, unless the user explicitly asked you to "plan first then execute" / "先 plan 再 auto" / autonomous execution: in that case, after presenting the plan, call the switch_mode tool with mode="auto" to switch back to auto mode WITHIN THE SAME TURN and continue implementing the plan yourself (your write/edit/command/memory-write tools become available again immediately). The user will see no approval prompt because you self-switched.
146
- - If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP. Do NOT ask the user for confirmation or approval in your text reply (e.g. "is this plan OK?", "shall I proceed?", "需要你确认") — the REPL automatically shows an approval prompt after you STOP, so asking in text is redundant and forces the user to answer twice. Just present the plan and end your reply.`;
195
+ export function getPlanModeSuffix() {
196
+ return buildPlanModeSuffix();
197
+ }
147
198
  export const config = {
148
199
  baseURL: requireEnv('LLM_BASE_URL'),
149
200
  apiKey: requireEnv('LLM_API_KEY'),
150
201
  model: process.env.LLM_MODEL || 'gpt-4o-mini',
151
202
  maxTokens: process.env.MAX_TOKENS ? Number(process.env.MAX_TOKENS) : undefined,
152
- systemPrompt: SYSTEM_PROMPT,
203
+ // 用 getter 而非 buildBasePrompt() 立即求值:因为本对象字面量求值时 buildBasePrompt 读 config.memoryEnabled,
204
+ // 而 config 还没完成初始化(TDZ)。Getter 让每次访问都现拼,运行时 /memory_switch 立即生效。
205
+ get systemPrompt() {
206
+ return buildBasePrompt();
207
+ },
153
208
  contextWindowTokens: Number(process.env.CONTEXT_WINDOW_TOKENS) || 128000,
154
209
  compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
155
210
  includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
156
211
  autoCompact: process.env.AUTO_COMPACT !== 'false',
157
212
  contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
213
+ contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE !== 'false',
214
+ contextLifecycle: process.env.MOCODE_LIFECYCLE !== 'false',
215
+ contextBudget: process.env.MOCODE_BUDGET_SCHEDULER !== 'false',
158
216
  autoReflect: process.env.AUTO_REFLECT !== 'false',
217
+ memoryEnabled: process.env.MEMORY_ENABLED === 'true',
159
218
  reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
160
219
  maxSteps: Number(process.env.MAX_STEPS) || 200,
161
220
  subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || 50,
@@ -196,3 +255,26 @@ export function updateModelConfig(opts) {
196
255
  process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
197
256
  }
198
257
  }
258
+ /**
259
+ * 记忆子系统总开关:单一来源。/memory_switch、/memory_status、buildSystemPrompt、
260
+ * tools/builtins/index.ts、tools/constants.ts 的 plan-mode 列表都从这里查。
261
+ * 默认 false(新用户零侵入)。
262
+ */
263
+ export function isMemoryEnabled() {
264
+ return config.memoryEnabled;
265
+ }
266
+ /**
267
+ * 切换记忆子系统开关(/memory_switch on|off 调)。
268
+ * - 更新 config 单例字段(其它模块下次调 isMemoryEnabled() 即拿新值)。
269
+ * - 同步 process.env.MEMORY_ENABLED(下次启动 loadEnvFiles 不被文件回填)。
270
+ * 持久化(写 ~/.mocode/config 的 MEMORY_ENABLED 键)由调用方走 writeConfigKeys。
271
+ *
272
+ * 注:开关切换对当前会话的 tool list / 已拼好的 systemPrompt 不会自动重算 —
273
+ * 工具表在 REPL 启动时构建,systemPrompt 在每轮 chat() 拼时按 isMemoryEnabled()
274
+ * 现查现拼(关掉时该轮拼出来的 prompt 即不带 memory_* 段)。所以切换在「下一轮
275
+ * agent 调用」起即时生效,本轮已发出的请求不会回滚。
276
+ */
277
+ export function updateMemoryConfig(enabled) {
278
+ config.memoryEnabled = enabled;
279
+ process.env.MEMORY_ENABLED = enabled ? 'true' : 'false';
280
+ }