mocode-ai 1.6.4 → 1.6.5

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.
Files changed (64) hide show
  1. package/README.md +376 -365
  2. package/README.zh-CN.md +13 -2
  3. package/dist/agent/model-turn.js +14 -1
  4. package/dist/agent/run-coordinator.js +5 -0
  5. package/dist/agent/spawn.js +38 -5
  6. package/dist/agent/stages/model-runner.js +1 -1
  7. package/dist/agent/stages/tool-dispatcher.js +1 -0
  8. package/dist/agent/work-discipline.js +3 -1
  9. package/dist/config/index.js +52 -2
  10. package/dist/config/presets.js +40 -1
  11. package/dist/config/profiles.js +9 -1
  12. package/dist/context/clearing.js +53 -0
  13. package/dist/context/relevance.js +6 -0
  14. package/dist/context/text-search.js +113 -0
  15. package/dist/llm/index.js +35 -8
  16. package/dist/llm/providers/anthropic.js +14 -4
  17. package/dist/llm/reasoning.js +141 -0
  18. package/dist/memory/reflect.js +1 -1
  19. package/dist/memory/store.js +31 -28
  20. package/dist/models/budgets.js +11 -0
  21. package/dist/models/catalog.js +130 -0
  22. package/dist/models/map-preset.js +50 -0
  23. package/dist/models/model-panel.js +227 -0
  24. package/dist/models/protocol.js +44 -0
  25. package/dist/models/reasoning-cap.js +88 -0
  26. package/dist/models/search.js +58 -0
  27. package/dist/models/types.js +8 -0
  28. package/dist/repl/commands/effort.js +81 -0
  29. package/dist/repl/commands/model-actions.js +138 -0
  30. package/dist/repl/commands/model-catalog.js +67 -0
  31. package/dist/repl/commands/model-panel-ui.js +107 -0
  32. package/dist/repl/commands/model.js +17 -0
  33. package/dist/repl/commands/registry.js +6 -0
  34. package/dist/repl/commands/session.js +54 -0
  35. package/dist/repl/commands/stats.js +28 -0
  36. package/dist/repl/commands.js +3 -0
  37. package/dist/repl/runtime.js +5 -1
  38. package/dist/repl/status-bar.js +4 -2
  39. package/dist/rollback/store.js +17 -0
  40. package/dist/runtime/runtime.js +31 -1
  41. package/dist/session/compact.js +84 -3
  42. package/dist/session/index.js +2 -0
  43. package/dist/session/retention.js +297 -0
  44. package/dist/session/scheduler.js +30 -3
  45. package/dist/session/store.js +108 -6
  46. package/dist/session/usage-stats.js +90 -0
  47. package/dist/skills/activation.js +2 -0
  48. package/dist/skills/builtin-skills.js +153 -0
  49. package/dist/skills/discover.js +3 -1
  50. package/dist/skills/runner.js +4 -1
  51. package/dist/tools/builtins/index.js +4 -0
  52. package/dist/tools/builtins/read-file.js +19 -1
  53. package/dist/tools/builtins/session-search.js +33 -0
  54. package/dist/tools/builtins/task.js +2 -0
  55. package/dist/tools/constants.js +8 -0
  56. package/dist/tools/read-dedup.js +62 -0
  57. package/dist/tools/tool-runtime.js +1 -0
  58. package/dist/ui/batch.js +109 -103
  59. package/dist/ui/fuzzy-picker.js +227 -0
  60. package/dist/ui/hierarchical-picker.js +302 -0
  61. package/dist/ui/intervention.js +11 -1
  62. package/dist/ui/layout-internal/statusbar.js +18 -7
  63. package/dist/ui/render.js +2 -0
  64. package/package.json +1 -1
package/README.zh-CN.md CHANGED
@@ -25,6 +25,15 @@ mocode 自己探索代码、读写改文件、执行命令、联网查资料,以
25
25
 
26
26
  <p align="center"><img src="./assets/demo-build-pomodoro.gif" alt="mocode 从零搭建番茄钟并通过浏览器截图自检" width="100%"></p>
27
27
 
28
+ ## 效率:更快、更省 token
29
+
30
+ - **两级模型面板** — 裸 `/model` 先列厂商、Enter 进入后再选模型;搜索是作用域内过滤——顶层只匹配**厂商名**,厂商内只匹配**模型名**,不再把厂商和模型混在一个结果里。
31
+ - **思考强度 `/effort`** — `off / low / medium / high / auto` 五档归一,按目标模型自动翻译成各家方言(Anthropic / OpenAI o·gpt-5 / Qwen3 / GLM / DeepSeek-R1 / 豆包);不认识的模型或第三方网关**不下发**未知字段,避免 400。
32
+ - **用量统计 `/stats`** — 本会话缓存命中率、分层 token 用量与压缩次数,基于后端真实上报,不靠估算。
33
+ - **缓存安全的压缩分叉** — 能整段复用旧对话时优先走 fork,保住 prompt-cache 前缀、减少重复计费;不满足条件再回落摘要(`MOCODE_COMPACT_FORK=false` 关闭)。
34
+ - **重复读取去重** — 同一轮对同一文件的重复读取命中缓存,不再为相同内容重复付费(`MOCODE_READ_DEDUP=false` 关闭)。
35
+ - **子 Agent 深度闸** — 递归派生默认最多 3 层(`SUB_AGENT_MAX_DEPTH` 可调),防止子 agent 无限膨胀。
36
+
28
37
  ## 架构
29
38
 
30
39
  MoCode 是一个分层的自治运行时:终端交互层驱动 Agent 内核,内核通过受控能力平面执行真实操作,持久化认知层则让长任务和跨会话工作保持连贯。
@@ -109,7 +118,7 @@ mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
109
118
  - **会话持久化** — 每轮自动落盘,`--resume` / `/resume` 续接历史会话
110
119
  - **Skills 系统** — 自动扫描 `~/.mocode/skills/` 等目录,description 注入系统提示,模型按需调 `use_skill` 加载完整指令(渐进式披露:先看简介,任务相关才加载正文)
111
120
  - **可选桌宠** — 独立悬浮窗(`/pet`)显示一个小角色,镜像 agent 活动(空闲 / 思考 / 跑工具 / 等人工),独立进程走 WebSocket,`/pet quit` 完全关闭。挂在终端外,绝不挡终端。
112
- - **斜杠命令** — `/exit` `/clear` `/cd` `/context` `/skills` `/compact` `/resume` `/rollback` `/memory` `/reflect` `/init` `/theme` `/model` `/plan` `/auto` `/pet`,输入时下拉过滤
121
+ - **斜杠命令** — `/exit` `/clear` `/cd` `/context` `/skills` `/compact` `/resume` `/rollback` `/memory` `/reflect` `/init` `/theme` `/model` `/effort` `/stats` `/plan` `/auto` `/pet`,输入时下拉过滤
113
122
 
114
123
  ## 使用文档
115
124
 
@@ -295,7 +304,9 @@ dev_server stop id=srv-xxxx
295
304
  | `/memory` | 看记忆库:条目数 + 近期索引 |
296
305
  | `/memory_switch` | 允许/禁止 memory 自动路由并切换 Memory Index;下一真实用户轮生效 |
297
306
  | `/reflect` | 手动触发一次后台记忆反思 pass |
298
- | `/model` | 配置大模型(baseURL / apiKey / model / 上下文窗口),即时生效 + 持久化 |
307
+ | `/model` | 两级面板切换模型(先选厂商再选模型;顶层按厂商名、厂商内按模型名过滤);也可配置 baseURL / apiKey / 上下文窗口,即时生效 + 持久化 |
308
+ | `/effort` | 设置思考强度 off/low/medium/high/auto(如 `/effort high`);未识别模型不下发参数 |
309
+ | `/stats` | 本会话用量:缓存命中率 / 分层 token / 压缩次数 |
299
310
  | `/init` | 扫描项目生成 `AGENTS.md` 项目记忆(发给 agent 执行) |
300
311
  | `/theme` | 切换颜色主题(↑↓ · Enter,或 `/theme <name>` 直切) |
301
312
  | `/plan` | 切到 plan 模式(只读探查 + 产出计划,审批后切 auto 执行) |
@@ -1,5 +1,8 @@
1
1
  import { estimatePromptTokens, estimateTokens, isContextLengthError, } from '../llm/index.js';
2
2
  import { visionBatch, visionKeep } from '../config/index.js';
3
+ import { getActiveSessionStore } from '../session/store.js';
4
+ import { getCurrentSessionId } from '../session/state.js';
5
+ import { toUsageRecord } from '../session/usage-stats.js';
3
6
  /**
4
7
  * 沿 cause 链(≤3 层)取第一个 errno,供 trace 取证。
5
8
  * undici 把底层 errno 挂在 cause 上(`TypeError: fetch failed` → cause `read ECONNRESET`),
@@ -18,7 +21,7 @@ function traceCauseCode(err) {
18
21
  }
19
22
  /** Executes context preparation plus exactly one model step, including the single overflow retry path. */
20
23
  export async function runModelTurn(input) {
21
- const { opts, ctx, history, historyManager, runtimeContextState, scheduler, contextTrimmer, modelRunner, activeTools, runPolicy, step, cacheState, turnLifecycle, cancellationLifecycle, rebuildHistoryIndexes, } = input;
24
+ const { opts, ctx, history, historyManager, runtimeContextState, scheduler, contextTrimmer, modelRunner, activeTools, runPolicy, step, readDedup, cacheState, turnLifecycle, cancellationLifecycle, rebuildHistoryIndexes, } = input;
22
25
  const { signal, onContextUpdate, hooks } = opts;
23
26
  const { usageMeter, emitTrace } = turnLifecycle;
24
27
  const requestBaseURL = ctx.config.baseURL;
@@ -37,6 +40,8 @@ export async function runModelTurn(input) {
37
40
  // 视觉滑动窗口**必须剪在 trim 之前**:contextTrimmer.trim() 拿的是 historyManager.snapshot(),
38
41
  // 若先 trim,预算口径还是未剪的旧数组,80% 压力线照旧被图像撑爆(design-notes/vision-window.md §2.1)。
39
42
  // 只在这一个地方剪;下面的 overflow 重试路径读同一个 snapshot,会自动受益。
43
+ runtimeContextState.currentStep = step;
44
+ readDedup.beginStep(step);
40
45
  const visionKeepN = visionKeep();
41
46
  if (visionKeepN > 0 && historyManager.pruneVisionWindow({ keep: visionKeepN, batch: visionBatch(), step })) {
42
47
  rebuildHistoryIndexes();
@@ -52,6 +57,8 @@ export async function runModelTurn(input) {
52
57
  signal,
53
58
  });
54
59
  historyRebuilt = trimResult.kind === 'rebuild';
60
+ if (trimResult.kind === 'rebuild' || trimResult.kind === 'content')
61
+ readDedup.markContextChanged();
55
62
  const trimStats = trimResult.kind === 'aborted' ? {} : trimResult.stats;
56
63
  if (scheduler && trimStats.compactHistoryCalled) {
57
64
  emitTrace('compact', {
@@ -244,6 +251,12 @@ export async function runModelTurn(input) {
244
251
  });
245
252
  runtimeContextState.lastUsage = result.usage;
246
253
  usageMeter.add(result.usage);
254
+ // P1:per-step 用量落盘(失败静默,见 appendUsage)。estimatedTotal 用本次裸估算,
255
+ // 供 /stats 校验估算偏差。
256
+ const record = toUsageRecord(step, 'main', result.usage, stepPromptEst);
257
+ const sessionId = getCurrentSessionId();
258
+ if (record && sessionId)
259
+ getActiveSessionStore().appendUsage(sessionId, record);
247
260
  if (result.usage) {
248
261
  cacheState.lastStepPromptTokens = result.usage.promptTokens;
249
262
  if (result.usage.cachedTokens > 0)
@@ -13,6 +13,7 @@ import { runToolTurn } from './tool-turn.js';
13
13
  import { createTurnLifecycle } from './turn-lifecycle.js';
14
14
  import { parseArgs, argumentErrorHint, isParallelTool, isParallelOrchestrationCall, isResourceLockedCall, deniedOutcome, readDiffContext, pushToolResult, } from './tool-helpers.js';
15
15
  import { contextState, summarizeToolArguments } from '../session/index.js';
16
+ import { createReadDedup } from '../tools/read-dedup.js';
16
17
  import { createBudgetScheduler } from '../session/scheduler.js';
17
18
  import { invalidateArtifacts, rehydrateArtifacts } from '../context/index.js';
18
19
  import { createRelevancePruner } from '../context/relevance.js';
@@ -64,6 +65,8 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
64
65
  const { usageMeter, emitTrace, traceTurnId } = turnLifecycle;
65
66
  const toolTurnPlanState = { stepsSincePlanTouch: 0 };
66
67
  historyManager.appendUserTurn(userInput);
68
+ // P2:每用户 turn 一个重复读 scope,经 dispatcher → ToolContext 透传给 read_file。
69
+ const readDedup = createReadDedup();
67
70
  // The initial cancellation checkpoint is captured after the user turn and before any model/tool work.
68
71
  // Relevance and lifecycle collect provenance during normal work. Neither path
69
72
  // rewrites history; exact supersession is applied only by the pressure scheduler.
@@ -173,6 +176,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
173
176
  activeTools,
174
177
  runPolicy,
175
178
  step,
179
+ readDedup,
176
180
  cacheState: modelCacheState,
177
181
  turnLifecycle,
178
182
  cancellationLifecycle,
@@ -318,6 +322,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
318
322
  isDenied: isToolDeniedForStep,
319
323
  currentAllowedToolNames,
320
324
  delegation: delegationForOrchestrator,
325
+ readDedup,
321
326
  argumentErrorHint: (name) => argumentErrorHint(name, runtimeContextState),
322
327
  ...(opts.toolPolicy
323
328
  ? {
@@ -10,6 +10,7 @@
10
10
  // - 主屏渲染可选:TUI 激活时把子 agent 内部工具调用实时写入主内容区并复用 batch 折叠;
11
11
  // TUI 未激活(host 嵌入 / 非 TTY)时纯静默,中间过程只缓冲进 transcript。
12
12
  // - 独立 history 分支:子任务的工具噪声不回灌主对话,只有最终摘要回灌。
13
+ import { AsyncLocalStorage } from 'node:async_hooks';
13
14
  import { buildMocodeCorePrompt, isSubAgentHardDisabled } from '../config/index.js';
14
15
  import { getToolChatSchema } from '../tools/policy.js';
15
16
  import { effectiveSystemPrompt } from '../skills/index.js';
@@ -47,6 +48,11 @@ You are executing one delegated sub-task with the same engineering standards and
47
48
  * 中断:opts.signal 透传给子 runAgentCore——主 Ctrl+C 树杀子 agent(chat abort + 工具 abort)。
48
49
  * 子 agent 跑在主 signal 下,主 abort 即子 abort;子 agent 的 abortRestore 还原子 history + 模式。
49
50
  */
51
+ /** 当前委派深度(P4 闸2):根 agent 无 store=0,每进一层 spawnAgent +1。 */
52
+ const spawnDepthScope = new AsyncLocalStorage();
53
+ function currentSpawnDepth() {
54
+ return spawnDepthScope.getStore() ?? 0;
55
+ }
50
56
  export async function spawnAgent(opts) {
51
57
  const activeRuntime = opts.runtime ?? getActiveRuntime();
52
58
  const runtimeContext = opts.runtimeContext ?? activeRuntime?.context ?? getActiveAgentRuntimeContext() ?? defaultAgentRuntimeContext;
@@ -60,6 +66,20 @@ export async function spawnAgent(opts) {
60
66
  usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, reasoningTokens: 0 },
61
67
  };
62
68
  }
69
+ // 深度闸:达上限直接结构化失败(模型看到后会改用直接工具完成)。执行层拦截,不裁 schema。
70
+ const depth = currentSpawnDepth() + 1;
71
+ if (depth > runtimeContext.config.subAgentMaxDepth) {
72
+ const msg = `Sub-agent depth limit ${runtimeContext.config.subAgentMaxDepth} reached ` +
73
+ '(nested delegation blocked to prevent fork-style recursion); complete the task directly with your own tools.';
74
+ return {
75
+ summary: msg,
76
+ completed: false,
77
+ transcript: msg,
78
+ status: 'failed',
79
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, reasoningTokens: 0 },
80
+ changedFiles: [],
81
+ };
82
+ }
63
83
  const maxSteps = opts.maxSteps ?? runtimeContext.config.subAgentMaxSteps;
64
84
  const requested = opts.tools === undefined ? null : new Set(opts.tools);
65
85
  const shared = opts.delegation && opts.delegation.history.length > 0 && opts.delegation.history[0]?.role === 'system'
@@ -172,8 +192,8 @@ export async function spawnAgent(opts) {
172
192
  const childIndex = parentId ? batch.getGroupChildIndex(opts.callId) : undefined;
173
193
  liveBatchId = batch.beginBatch(t('subagent.running'), {
174
194
  parentId,
175
- // 子批摘要行缩进 = 两层 entry 缩进(随 ENTRY_INDENT 联动,不硬编码空格数)。
176
- indent: parentId ? batch.SUB_BATCH_INDENT : undefined,
195
+ // 缩进由 beginBatch 按父层级自动推导:组容器批下 = 两层 entry 缩进;
196
+ // 普通子 agent 批下再嵌套 = 父缩进 + 两层(树状逐层加深)。
177
197
  groupChildIndex: childIndex,
178
198
  running: true,
179
199
  });
@@ -205,6 +225,8 @@ export async function spawnAgent(opts) {
205
225
  layout.contentWrite('\n');
206
226
  };
207
227
  let lastChar = '';
228
+ // P4 闸3:只收集本 agent 树自己产生的改动(钩子按真实执行触发),不靠整轮全局快照。
229
+ const nestedChangedFiles = new Set();
208
230
  const hooks = {
209
231
  onText: (s) => {
210
232
  writeBuf(s); // 缓冲流式正文(无 markdown 渲染,原始文本)
@@ -225,7 +247,10 @@ export async function spawnAgent(opts) {
225
247
  ensureQuietLine(opts.quietLabel ?? t('skill.executing', { name: opts.prompt.slice(0, 40) }));
226
248
  ensureLiveBatch();
227
249
  if (liveBatchId) {
228
- batch.recordCall(liveBatchId, tc.name, summary);
250
+ // 登记内部调用的 tool_call id:子 agent 嵌套派生时,孙 spawnAgent 据此反查到
251
+ // 本子批,把孙批挂到对应的内部 sub-agent entry 下(而非游离到 buffer 末尾)。
252
+ batch.bindCall(tc.id, liveBatchId);
253
+ batch.recordCall(liveBatchId, tc.name, summary, tc.id);
229
254
  // 默认折叠,只刷新摘要行计数(glob/read_file 数量),不展开明细列表。
230
255
  batch.showLiveBatch(liveBatchId, liveLayout());
231
256
  }
@@ -237,7 +262,7 @@ export async function spawnAgent(opts) {
237
262
  if (liveBatchId) {
238
263
  // 子 agent 批不展示 diff(其写入已直接落工作区,由主 agent 的当前回滚事务统一追踪);
239
264
  // 空 preview 用占位标记 entry 已完成,避免折叠后摘要仍显示"运行中"。
240
- batch.recordResult(liveBatchId, tc.name, preview || t('toolSummary.noOutput'), null, output, isToolErrorOutput(output));
265
+ batch.recordResult(liveBatchId, tc.name, preview || t('toolSummary.noOutput'), null, output, isToolErrorOutput(output), tc.id);
241
266
  batch.showLiveBatch(liveBatchId, liveLayout());
242
267
  }
243
268
  },
@@ -286,7 +311,7 @@ export async function spawnAgent(opts) {
286
311
  // 与主 agent 完全同源:写操作直接落在工作区,进入主 agent 当前轮次的同一回滚事务
287
312
  // (spawn 不调 beginTurn)。没有 overlay 拷贝/ChangeSet 合并这一步——那是旧 read/write
288
313
  // 双模式的产物,子 agent 不再受限,也就不需要"先隔离再合并"。
289
- const result = await runtime.run({
314
+ const runResult = () => runtime.run({
290
315
  turn: 'inherit',
291
316
  history,
292
317
  userInput,
@@ -296,11 +321,18 @@ export async function spawnAgent(opts) {
296
321
  toolsOverride,
297
322
  runtimeAllowedToolNames,
298
323
  contextState: localContextState,
324
+ // P4 闸3:run-coordinator 只消费顶层 onToolOutcome(非 hooks),在此收集本树真实改动。
325
+ onToolOutcome: (_tool, _args, outcome) => {
326
+ for (const f of outcome.changedFiles ?? [])
327
+ nestedChangedFiles.add(f);
328
+ },
299
329
  suppressOpeningAnalysis: true, // 子代理不注入「开场分析」:仅主线面对用户的首次响应用
300
330
  // 子代理不注入主会话「会话状态」(plan + 笔记):那是主 agent 的工作面,委派消息已带齐
301
331
  // 子任务所需上下文,重复注入只白付 token。
302
332
  suppressSessionState: true,
303
333
  });
334
+ // 深度 scope:子 agent 内再派生子 agent 时 ALS 读到此 depth。
335
+ const result = await spawnDepthScope.run(depth, runResult);
304
336
  const status = opts.signal?.aborted || result.terminationReason === 'aborted'
305
337
  ? 'aborted'
306
338
  : result.completed
@@ -318,5 +350,6 @@ export async function spawnAgent(opts) {
318
350
  cachedTokens: result.usage?.cachedTokens ?? 0,
319
351
  reasoningTokens: result.usage?.reasoningTokens ?? 0,
320
352
  },
353
+ changedFiles: [...nestedChangedFiles],
321
354
  };
322
355
  }
@@ -7,7 +7,7 @@ class ChatModelRunner {
7
7
  this.transport = transport;
8
8
  }
9
9
  run(request, signal) {
10
- return this.transport(request.history.slice(), request.handlers, signal, request.tools.slice());
10
+ return this.transport(request.history.slice(), request.handlers, signal, request.tools.slice(), request.reasoningEffort ? { reasoningEffort: request.reasoningEffort } : undefined);
11
11
  }
12
12
  }
13
13
  class LegacyChatModelRunner extends ChatModelRunner {
@@ -46,6 +46,7 @@ class LegacyCompatibleToolDispatcher {
46
46
  callId: call.id,
47
47
  allowedToolNames: request.currentAllowedToolNames(),
48
48
  delegation: request.delegation(),
49
+ readDedup: request.readDedup,
49
50
  ...(hint ? { argumentErrorHint: hint } : {}),
50
51
  ...(onLockAcquired ? { onLockAcquired } : {}),
51
52
  });
@@ -41,9 +41,11 @@ Call \`ask_human\` before coding only when repository evidence cannot resolve a
41
41
  3. multiple reasonable options that materially change product behavior;
42
42
  4. the request itself admits two or more materially different readings that lead to different deliverables (do not silently pick one and guess).
43
43
 
44
+ When the request is ambiguous, vague, or missing key information, first load the \`brainstorming\` skill and clarify the need through a short dialogue (one question at a time) before starting work.
45
+
44
46
  For naming and implementation details, follow repository precedent and choose the safest reversible default. Disclose any consequential assumption.
45
47
 
46
- Budget: at most 2 \`ask_human\` calls per turn. Beyond that, use the safest reversible default and disclose it in the final reply.`;
48
+ Budget: during requirement clarification (the \`brainstorming\` workflow) ask as many questions as genuinely advance understanding; during execution after requirements are clear, use at most 2 \`ask_human\` calls per turn, then fall back to the safest reversible default and disclose it in the final reply.`;
47
49
  /** Advisory guidance shared by main and sub-agents. */
48
50
  export function buildWorkDisciplineSection(_modelFamily) {
49
51
  return `${CORE_SECTION}\n\n${ASK_WHITELIST_SECTION}`;
@@ -2,6 +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 { parseReasoningEffort, getScopedEffort } from '../llm/reasoning.js';
6
+ import { getActiveSkill } from '../skills/activation.js';
5
7
  import { getCurrentSessionId } from '../session/state.js';
6
8
  import { getNotesFilePath, extractActiveNotesSections } from '../session/notes.js';
7
9
  import { buildGuiActionsSection } from '../session/gui-actions.js';
@@ -43,7 +45,7 @@ function loadEnvFiles() {
43
45
  process.env[k] = v;
44
46
  }
45
47
  }
46
- // 在 loadEnvFiles 回填前捕获:MOCODE_THEME / MOCODE_LANGUAGE 是否由 shell 设置。
48
+ // 在 loadEnvFiles 回填前先捕获:MOCODE_THEME / MOCODE_LANGUAGE 是否由 shell 显式 export(决定后续优先级提示)。
47
49
  const themeFromShell = process.env.MOCODE_THEME !== undefined;
48
50
  export const languageFromShell = process.env.MOCODE_LANGUAGE !== undefined;
49
51
  // 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
@@ -56,6 +58,7 @@ const LLM_ENV_KEYS = [
56
58
  'LLM_MODEL',
57
59
  'CONTEXT_WINDOW_TOKENS',
58
60
  'ANTHROPIC_PROMPT_CACHE',
61
+ 'REASONING_EFFORT',
59
62
  ];
60
63
  export const DEFAULT_CONTEXT_WINDOW_TOKENS = 256000;
61
64
  const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
@@ -492,7 +495,8 @@ ${buildVoiceSection()}
492
495
  - Stop immediately when no more tools are needed; give conclusions directly.
493
496
  - **Do not stop prematurely during exploration**: if you started investigating but haven't gathered enough information to answer the user's question, keep calling tools. Only stop when you have sufficient evidence or hit a dead end.
494
497
  - **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
495
- - 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.`;
498
+ - 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.
499
+ - Definition of done: never declare a multi-feature request finished just because the code exists. Each distinct user request listed in the plan must be actually verified (the relevant check run / observed result), not merely implemented; mark plan steps \`[x]\` only after that verification. If the user supplied an acceptance list, every item on it must pass. Leave unverified items explicitly listed as pending instead of collapsing them into "done".`;
496
500
  // 动态段(置于末尾):AGENTS.md 项目记忆 + notepad 索引/说明。工具簇特定指导由
497
501
  // ToolPolicyController.reminder() 按当前 turn 的 route 注入,避免旧全局 profile 与真实 schema 分裂。
498
502
  // 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
@@ -582,10 +586,17 @@ export const config = {
582
586
  ? process.env.ANTHROPIC_PROMPT_CACHE !== 'false'
583
587
  : (__activePreset?.anthropicPromptCache ?? process.env.ANTHROPIC_PROMPT_CACHE !== 'false'),
584
588
  autoCompact: process.env.AUTO_COMPACT !== 'false',
589
+ reasoningEffort: llmKeysFromShell.includes('REASONING_EFFORT')
590
+ ? (parseReasoningEffort(process.env.REASONING_EFFORT) ?? 'auto')
591
+ : (__activePreset?.reasoningEffort ?? parseReasoningEffort(process.env.REASONING_EFFORT) ?? 'auto'),
592
+ compactFork: process.env.MOCODE_COMPACT_FORK !== 'false',
593
+ readDedup: process.env.MOCODE_READ_DEDUP !== 'false',
585
594
  contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
586
595
  contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE !== 'false',
587
596
  contextLifecycle: process.env.MOCODE_LIFECYCLE !== 'false',
588
597
  contextBudget: process.env.MOCODE_BUDGET_SCHEDULER !== 'false',
598
+ lowPressureRatio: Math.min(0.95, Math.max(0.1, Number(process.env.MOCODE_LOW_PRESSURE_RATIO) || 0.6)),
599
+ toolClearing: process.env.MOCODE_TOOL_CLEARING !== 'false',
589
600
  autoReflect: process.env.AUTO_REFLECT === 'true',
590
601
  memoryEnabled: process.env.MEMORY_ENABLED === 'true',
591
602
  reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
@@ -593,6 +604,7 @@ export const config = {
593
604
  subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
594
605
  subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || Number(process.env.MAX_STEPS) || 1000,
595
606
  subAgentConcurrency: Math.max(1, Number(process.env.SUB_AGENT_CONCURRENCY) || 5),
607
+ subAgentMaxDepth: Math.max(1, Number(process.env.SUB_AGENT_MAX_DEPTH) || 3),
596
608
  frontendToolsEnabled: process.env.MOCODE_FRONTEND_TOOLS_ENABLED === 'true',
597
609
  computerUseEnabled: process.env.MOCODE_COMPUTER_USE_ENABLED === 'true',
598
610
  mcpEnabled: process.env.MOCODE_MCP_ENABLED !== 'false',
@@ -636,6 +648,38 @@ export function pinSessionModel() {
636
648
  export function getActiveModel() {
637
649
  return sessionModel ?? config.model;
638
650
  }
651
+ /**
652
+ * 会话级思考强度钉死(P3),与 sessionModel 同构:窗口启动 pinSessionEffort() 捕获,
653
+ * /effort 显式修改同步更新钉死值;其它窗口的设置互不影响。
654
+ */
655
+ let sessionEffort = null;
656
+ /** REPL 启动时调用一次。 */
657
+ export function pinSessionEffort() {
658
+ sessionEffort = config.reasoningEffort;
659
+ }
660
+ /** 运行中 agent 实际使用的思考强度。 */
661
+ export function getActiveEffort() {
662
+ return sessionEffort ?? config.reasoningEffort;
663
+ }
664
+ /**
665
+ * 生效思考强度统一解析(P3 §4.8):
666
+ * 显式 per-request 参数(compact/reflect 的 low)> fork ALS scope(skill 子树)
667
+ * > inline 激活 skill 的 effort > 会话级。
668
+ * shell env 显式设置 REASONING_EFFORT 时,skill effort 不参与(对齐 Claude Code:env 最高)。
669
+ */
670
+ export function effectiveReasoningEffort(explicit) {
671
+ if (explicit !== undefined)
672
+ return explicit;
673
+ const scoped = getScopedEffort();
674
+ if (scoped)
675
+ return scoped;
676
+ if (!config.llmKeysFromShell.includes('REASONING_EFFORT')) {
677
+ const skillEffort = getActiveSkill()?.effort;
678
+ if (skillEffort)
679
+ return skillEffort;
680
+ }
681
+ return getActiveEffort();
682
+ }
639
683
  /**
640
684
  * 运行时更新模型相关配置(/model 命令调)。
641
685
  * - 更新 config 对象字段(即时生效:chat() 读 config.model,reconfigureClient 读 config.baseURL/apiKey)。
@@ -673,6 +717,12 @@ export function updateModelConfig(opts) {
673
717
  config.anthropicPromptCache = opts.anthropicPromptCache;
674
718
  process.env.ANTHROPIC_PROMPT_CACHE = opts.anthropicPromptCache ? 'true' : 'false';
675
719
  }
720
+ if (opts.reasoningEffort !== undefined) {
721
+ config.reasoningEffort = opts.reasoningEffort;
722
+ if (sessionEffort !== null)
723
+ sessionEffort = opts.reasoningEffort;
724
+ process.env.REASONING_EFFORT = opts.reasoningEffort;
725
+ }
676
726
  }
677
727
  /** legacy 嵌入路径的 profile 查询;官方主 Agent 不读取。 */
678
728
  export function getActiveProfile() {
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { parseReasoningEffort } from '../llm/reasoning.js';
4
5
  /**
5
6
  * 多模型预设(`/model save <name>` 保存的命名配置)的纯 I/O 叶子。
6
7
  *
@@ -78,6 +79,41 @@ export function parsePreset(raw) {
78
79
  }
79
80
  const provider = obj.provider === 'anthropic' ? 'anthropic' : 'openai';
80
81
  const anthropicPromptCache = provider === 'anthropic' && obj.anthropicPromptCache !== false;
82
+ // reasoningEffort 缺省 = auto;显式写了非法值按现有校验风格报错(坏文件由 listPresets 跳过)。
83
+ let reasoningEffort;
84
+ if (obj.reasoningEffort !== undefined) {
85
+ reasoningEffort = parseReasoningEffort(obj.reasoningEffort);
86
+ if (!reasoningEffort)
87
+ throw new Error(`预设 ${name}: reasoningEffort 非法(off|low|medium|high|auto)`);
88
+ }
89
+ // ── 目录可选字段:只在「存在且类型正确」时采纳,缺失/错误即 undefined,不报错 ──
90
+ const extra = {};
91
+ if (typeof obj.catalogProvider === 'string' && obj.catalogProvider)
92
+ extra.catalogProvider = obj.catalogProvider;
93
+ if (typeof obj.catalogModel === 'string' && obj.catalogModel)
94
+ extra.catalogModel = obj.catalogModel;
95
+ if (obj.capabilities && typeof obj.capabilities === 'object') {
96
+ const c = obj.capabilities;
97
+ const caps = {};
98
+ if (typeof c.reasoning === 'boolean')
99
+ caps.reasoning = c.reasoning;
100
+ if (typeof c.toolCall === 'boolean')
101
+ caps.toolCall = c.toolCall;
102
+ if (typeof c.attachment === 'boolean')
103
+ caps.attachment = c.attachment;
104
+ if (Array.isArray(c.reasoningOptions))
105
+ caps.reasoningOptions = c.reasoningOptions;
106
+ extra.capabilities = caps;
107
+ }
108
+ if (obj.pricing && typeof obj.pricing === 'object') {
109
+ const p = obj.pricing;
110
+ const pricing = {};
111
+ for (const k of ['input', 'output', 'cacheRead']) {
112
+ if (typeof p[k] === 'number' && Number.isFinite(p[k]))
113
+ pricing[k] = p[k];
114
+ }
115
+ extra.pricing = pricing;
116
+ }
81
117
  return {
82
118
  name,
83
119
  provider,
@@ -86,6 +122,8 @@ export function parsePreset(raw) {
86
122
  model,
87
123
  contextWindow: Math.floor(contextWindow),
88
124
  anthropicPromptCache,
125
+ ...(reasoningEffort ? { reasoningEffort } : {}),
126
+ ...extra,
89
127
  };
90
128
  }
91
129
  /** 读单个预设;不存在抛错。 */
@@ -217,7 +255,8 @@ export function migrateCurrentToPreset(input) {
217
255
  p.apiKey === input.apiKey &&
218
256
  p.model === input.model &&
219
257
  p.contextWindow === input.contextWindow &&
220
- p.anthropicPromptCache === anthropicPromptCache);
258
+ p.anthropicPromptCache === anthropicPromptCache &&
259
+ (p.reasoningEffort ?? 'auto') === 'auto');
221
260
  if (dup)
222
261
  return null;
223
262
  // 'default' 已被占 → 用户已显式起过预设,无需老数据迁入;返回 null 让调用方跳过即可。
@@ -19,7 +19,15 @@ export const TOOL_GROUPS = {
19
19
  web: ['web_search', 'web_fetch'],
20
20
  frontend: ['browser', 'screenshot'],
21
21
  computer: ['computer'],
22
- memory: ['memory_save', 'memory_search', 'memory_list', 'memory_update', 'memory_forget', 'memory_graph'],
22
+ memory: [
23
+ 'memory_save',
24
+ 'memory_search',
25
+ 'memory_list',
26
+ 'memory_update',
27
+ 'memory_forget',
28
+ 'memory_graph',
29
+ 'session_search',
30
+ ],
23
31
  subagent: ['sub-agent'],
24
32
  };
25
33
  // ── LLM 自动工具路由 ──────────────────────────────────────────────────────
@@ -0,0 +1,53 @@
1
+ // Tool-result clearing: 可重取结果的低成本清除(Anthropic 三原语之一)。
2
+ //
3
+ // 与 relevance pruner 的区别:pruner 只在有「精确的新替代」时 stub;clearing 更宽——
4
+ // 冷区中来自可重取工具的结果,无论有没有新替代,内容都可丢弃(需要时重新调用即可),
5
+ // 只保留「调用发生过」的 tombstone。不删消息、不改 tool_call_id 配对。
6
+ //
7
+ // 在低压阶段(60%)运行:零 LLM 成本、不依赖摘要质量。热区(最近 hotTurnWindow 个
8
+ // user turn)不动。
9
+ import { toText } from './utils.js';
10
+ /** 结果可随时重取的工具:纯读、无副作用。 */
11
+ export const REFETCHABLE_TOOLS = new Set(['read_file', 'grep', 'glob', 'web_search', 'web_fetch']);
12
+ /** Tombstone 前缀,与 relevance stub 同属 ⌦[ 家族,其余子系统据此识别已清除内容。 */
13
+ const CLEARED_PREFIX = '⌦[已清除:';
14
+ /** 回溯 tool_call_id → 产生该结果的工具名(同 relevance.ts callAt 的最小版本)。 */
15
+ function toolNameAt(history, idx) {
16
+ const tcId = history[idx]?.tool_call_id;
17
+ if (!tcId)
18
+ return null;
19
+ for (let j = idx - 1; j >= 1; j--) {
20
+ if (history[j].role !== 'assistant')
21
+ continue;
22
+ const calls = history[j].tool_calls;
23
+ const hit = calls?.find((tc) => tc?.id === tcId);
24
+ if (hit?.function?.name)
25
+ return hit.function.name;
26
+ }
27
+ return null;
28
+ }
29
+ /**
30
+ * 清除冷区可重取工具的结果内容。
31
+ * @param coldBoundary 仅处理 index < coldBoundary 的消息(热区保留)。
32
+ * @returns 被清除的消息数。
33
+ */
34
+ export function clearRetrievableResults(history, coldBoundary) {
35
+ let cleared = 0;
36
+ const end = Math.min(coldBoundary, history.length);
37
+ for (let idx = 1; idx < end; idx++) {
38
+ const message = history[idx];
39
+ if (message.role !== 'tool' || !message.tool_call_id)
40
+ continue;
41
+ const content = toText(message.content);
42
+ if (!content || content.startsWith('⌦['))
43
+ continue;
44
+ const name = toolNameAt(history, idx);
45
+ if (!name || !REFETCHABLE_TOOLS.has(name))
46
+ continue;
47
+ message.content =
48
+ `${CLEARED_PREFIX}${name}] 原结果 ${content.length} 字符已清除(可重新调用获取)` +
49
+ ` · id …${message.tool_call_id.slice(-6)}⌫`;
50
+ cleared++;
51
+ }
52
+ return cleared;
53
+ }
@@ -258,10 +258,15 @@ export function computePruneStats(history) {
258
258
  let stubbed = 0;
259
259
  let originalChars = 0;
260
260
  let stubChars = 0;
261
+ let cleared = 0;
261
262
  for (const message of history) {
262
263
  if (message.role !== 'tool')
263
264
  continue;
264
265
  const content = toText(message.content);
266
+ if (content.startsWith('⌦[已清除:')) {
267
+ cleared++;
268
+ continue;
269
+ }
265
270
  const isPruneStub = content.startsWith(STUB_PREFIX);
266
271
  const isDigest = content.startsWith('⌦[摘要:');
267
272
  if (!isPruneStub && !isDigest)
@@ -280,5 +285,6 @@ export function computePruneStats(history) {
280
285
  originalTokens,
281
286
  stubChars,
282
287
  freedTokens: Math.max(0, originalTokens - stubTokens),
288
+ cleared,
283
289
  };
284
290
  }