mocode-ai 0.1.8 → 0.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 CHANGED
@@ -58,6 +58,8 @@ npm start
58
58
  mocode config
59
59
  ```
60
60
 
61
+ 也可直接 `mocode` 进入 REPL 后用 `/model` 命令配置(交互选后端预设 + 逐项填写,即时生效 + 持久化)。未配置时 REPL 仍能打开,会提示你跑 `/model`。
62
+
61
63
  也可手写配置文件。mocode 按以下优先级加载(后者覆盖前者,仅回填未设置的环境变量;shell 里 `export` 的永远最优先):
62
64
 
63
65
  1. `~/.mocode/config` — 全局(`mocode config` 写此文件)
@@ -149,6 +151,7 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
149
151
  | `/resume` | 续接已保存的会话 |
150
152
  | `/think` | 展开折叠思考段(`/think N`) |
151
153
  | `/rollback` | 菜单选轮次回滚(↑↓ · Enter) |
154
+ | `/model` | 配置大模型(baseURL / apiKey / model / 上下文窗口),即时生效 + 持久化 |
152
155
 
153
156
  输入 `/` 触发下拉菜单,继续打字过滤;Esc 取消。
154
157
 
@@ -9,7 +9,7 @@ import { chat, planChatTools, } from '../llm/index.js';
9
9
  import { executeTool } from '../tools/registry.js';
10
10
  import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
11
11
  import { getAgentMode, setAgentMode } from './mode.js';
12
- import { maybeCompact, contextState } from '../session/index.js';
12
+ import { maybeCompact, contextState, dropContextFromHistory } from '../session/index.js';
13
13
  import { optimizeToolResult } from '../context/index.js';
14
14
  import { config } from '../config/index.js';
15
15
  import { jailResolve } from '../sandbox/index.js';
@@ -107,6 +107,10 @@ export async function runAgentCore(opts) {
107
107
  const t0 = Date.now();
108
108
  let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
109
109
  history.push({ role: 'user', content: userInput });
110
+ // drop_context 工具的上下文剔除回调:闭包捕获 history,原地剔除无关旧 tool 结果。
111
+ // 保护由 dropContextFromHistory 内部保证:history[0](system)+ 当前轮(最后 user 及其后)永不剔除。
112
+ // 子 agent 也在自己的 history 上操作(子 agent 独立 history);skipRollback 不影响此行为。
113
+ const dropContext = (filter) => dropContextFromHistory(history, filter);
110
114
  // 本轮流式状态:首个正文 token 到达即停 spinner(思考期间 spinner 持续转「思考中…」,不写思考内容)。
111
115
  let mode = 'idle';
112
116
  let gotText = false;
@@ -202,7 +206,7 @@ export async function runAgentCore(opts) {
202
206
  while (j < calls.length && READ_TOOL_NAMES.has(calls[j].name))
203
207
  j++;
204
208
  const batch = calls.slice(i, j);
205
- const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
209
+ const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext }));
206
210
  for (let k = 0; k < batch.length; k++) {
207
211
  const tc = batch[k];
208
212
  hooks.onToolHeader?.(tc);
@@ -238,7 +242,7 @@ export async function runAgentCore(opts) {
238
242
  while (j < calls.length && calls[j].name === 'task')
239
243
  j++;
240
244
  const batch = calls.slice(i, j);
241
- const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
245
+ const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext }));
242
246
  // 先批量打印所有头 + 启 spinner(多 task 并发,spinner 只显一个,但 ● 头都打出来)
243
247
  for (const tc of batch) {
244
248
  hooks.onToolHeader?.(tc);
@@ -273,7 +277,7 @@ export async function runAgentCore(opts) {
273
277
  : null;
274
278
  const { preWriteOld, editStartLine } = readDiffContext(tc, parsed);
275
279
  hooks.onToolStart?.(tc.name);
276
- const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback });
280
+ const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext });
277
281
  hooks.onToolDone?.();
278
282
  hooks.onToolResult?.(tc, output, parsed, preWriteOld, editStartLine);
279
283
  pushToolResult(history, tc, output);
@@ -9,7 +9,7 @@ function ask(rl, q) {
9
9
  * 首跑配置向导:交互填 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL,写 ~/.mocode/config。
10
10
  * 只交互三键、保留文件里其它键(MAX_TOKENS / CONTEXT_WINDOW_TOKENS / MOCODE_THEME 等)。
11
11
  * prompt 全纯文本(readline 光标按字符算,不能含 ANSI)。
12
- * 由 index.ts 在 `mocode config` 时动态加载,故不 import config/index.ts(避免缺配置时 requireEnv
12
+ * 由 index.ts 在 `mocode config` 时动态加载,故不 import config/index.ts(避免触发 config 单例初始化 / loadEnvFiles,
13
13
  * 直接退出);只 import config/file.ts(纯 I/O 叶子,无 env 校验 / process.exit)。
14
14
  */
15
15
  export async function runConfigWizard() {
@@ -5,7 +5,7 @@ import dotenv from 'dotenv';
5
5
  /**
6
6
  * ~/.mocode/config 全局 dotenv 配置的读写叶子(纯 node:fs / node:os / node:path + dotenv)。
7
7
  *
8
- * 独立于 `config/index.ts`——**不**触发 requireEnv / process.exit,故 `commands/config.ts`
8
+ * 独立于 `config/index.ts`——**不**触发 config 单例初始化 / loadEnvFiles,故 `commands/config.ts`
9
9
  * (首跑向导,刻意不 import config/index.ts)与 `repl/index.ts`(`/theme` 持久化)都能安全共享。
10
10
  * 主题等 UI 偏好走 `updateConfigKey` 单键写;向导多键一次性写走 `writeConfigKeys`。
11
11
  */
@@ -30,14 +30,26 @@ function loadEnvFiles() {
30
30
  }
31
31
  // 在 loadEnvFiles 回填前捕获:MOCODE_THEME 是否由 shell 设置(决定 /theme 写文件是否下次启动生效)。
32
32
  const themeFromShell = process.env.MOCODE_THEME !== undefined;
33
+ // 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
34
+ // 仿 themeFromShell 模式:shell export 的环境变量在 loadEnvFiles 中不被回填(优先级最高),
35
+ // 故 /model 写入 ~/.mocode/config 的同名键下次启动会被 shell 值覆盖——据此给 dim 警告。
36
+ const LLM_ENV_KEYS = ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS'];
37
+ const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
33
38
  loadEnvFiles();
39
+ /**
40
+ * 取环境变量;缺则返回空字符串(不退出)。
41
+ * 历史上缺 LLM_BASE_URL/LLM_API_KEY 会 process.exit(1),但 /model 命令已能在 REPL 内配置模型,
42
+ * 故首次未配置也应让 REPL 起来,由开场提示引导用户跑 /model。发消息时 chat() 会抛错被 runTurn catch,不崩。
43
+ */
34
44
  function requireEnv(key) {
35
- const v = process.env[key];
36
- if (!v) {
37
- console.error(`\n[config] 缺少 ${key}。运行 \`mocode config\` 初始化,或在 ~/.mocode/config / <cwd>/.env 中设置(参考 .env.example)。\n`);
38
- process.exit(1);
39
- }
40
- return v;
45
+ return process.env[key] || '';
46
+ }
47
+ /**
48
+ * 模型是否已配置(baseURL + apiKey 非空)。REPL 开场据此决定是否提示 /model。
49
+ * 未配置时 config.model 仍回退 'gpt-4o-mini',但发消息会因 baseURL/apiKey 空而失败——由 runTurn catch 友好提示。
50
+ */
51
+ export function isModelConfigured() {
52
+ return !!config.baseURL && !!config.apiKey;
41
53
  }
42
54
  const PLATFORM_NOTE = (() => {
43
55
  if (process.platform === 'win32') {
@@ -76,6 +88,7 @@ ${PLATFORM_NOTE}
76
88
  - Use web_search for information beyond training data (new versions, news, real-time data, latest APIs); don't answer potentially outdated info from memory.
77
89
  - 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).
78
90
  - 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.
91
+ - **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.
79
92
 
80
93
  ## Failure Handling
81
94
  - Tools return errors as strings (edit_file no match or non-unique, run_command non-zero exit, etc.). Analyze the root cause, adjust, then retry — don't resend the same call verbatim.
@@ -111,7 +124,7 @@ You are in PLAN mode: investigate and design only — do NOT execute or change a
111
124
  - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Prefer codegraph when a .codegraph/ index exists.
112
125
  - 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
126
  - 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; the user will approve via a prompt and execution happens in a follow-up turn.`;
127
+ - 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
128
  export const config = {
116
129
  baseURL: requireEnv('LLM_BASE_URL'),
117
130
  apiKey: requireEnv('LLM_API_KEY'),
@@ -133,4 +146,31 @@ export const config = {
133
146
  searchBaseUrl: process.env.ANYSEARCH_BASE_URL || 'https://api.anysearch.com',
134
147
  theme: process.env.MOCODE_THEME || 'default',
135
148
  themeFromShell,
149
+ llmKeysFromShell,
136
150
  };
151
+ /**
152
+ * 运行时更新模型相关配置(/model 命令调)。
153
+ * - 更新 config 对象字段(即时生效:chat() 读 config.model,reconfigureClient 读 config.baseURL/apiKey)。
154
+ * - 同步 process.env(保持内存一致:其他读 process.env 的路径也拿到新值;且使新值在下次启动的
155
+ * loadEnvFiles 中被视为"已设",不被文件回填覆盖——即"优先拿这里的")。
156
+ * 持久化(写 ~/.mocode/config)由调用方走 writeConfigKeys,此处只管内存 + env。
157
+ * 重建 OpenAI 客户端(baseURL/apiKey 是构造时固化的实例字段)由调用方走 reconfigureClient。
158
+ */
159
+ export function updateModelConfig(opts) {
160
+ if (opts.model !== undefined) {
161
+ config.model = opts.model;
162
+ process.env.LLM_MODEL = opts.model;
163
+ }
164
+ if (opts.baseURL !== undefined) {
165
+ config.baseURL = opts.baseURL;
166
+ process.env.LLM_BASE_URL = opts.baseURL;
167
+ }
168
+ if (opts.apiKey !== undefined) {
169
+ config.apiKey = opts.apiKey;
170
+ process.env.LLM_API_KEY = opts.apiKey;
171
+ }
172
+ if (opts.contextWindowTokens !== undefined) {
173
+ config.contextWindowTokens = opts.contextWindowTokens;
174
+ process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
175
+ }
176
+ }
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // 单一事实源风格(仿 tools/constants.ts 的 READ_TOOL_NAMES / PLAN_DISABLED_TOOLS)。
9
9
  // 加新工具:在 BY_NAME 加一行;或靠形状启发自动识别。
10
- /** 工具名 → ContextKind 的强先验表(覆盖全部 17 内置工具)。 */
10
+ /** 工具名 → ContextKind 的强先验表(覆盖全部 18 内置工具)。 */
11
11
  const BY_NAME = {
12
12
  // tree:路径列表 → 缩进树
13
13
  glob: 'tree',
@@ -32,6 +32,7 @@ const BY_NAME = {
32
32
  write_file: 'status',
33
33
  ask_human: 'status',
34
34
  switch_mode: 'status',
35
+ drop_context: 'status',
35
36
  memory_save: 'status',
36
37
  memory_update: 'status',
37
38
  memory_forget: 'status',
package/dist/index.js CHANGED
@@ -2,7 +2,8 @@ import { exitAltScreen } from './ui/layout.js';
2
2
  import { checkAndMaybeUpdate } from './updater/index.js';
3
3
  // 终端恢复兜底:任一退出 / 中断 / 未捕获异常路径都要恢复 alt screen,避免残留备用屏 + 滚动区域。
4
4
  // exitAltScreen 幂等(未激活时空操作),故全局注册安全——进 alt screen 前的路径(如 --resume 列表、缺环境变量、`mocode config`)调用它无副作用。
5
- // 仅 layout 是叶子(不依赖 config),故静态导入安全;repl / session 依赖 config(requireEnv 缺项即退出),改动态按需加载——`mocode config` 才能在零配置下跑。
5
+ // 仅 layout 是叶子(不依赖 config),故静态导入安全;repl / session 依赖 config(模块加载触发 loadEnvFiles + config 单例初始化),
6
+ // 改动态按需加载——`mocode config` 向导只需读写文件(走 config/file.ts 叶子),不经 config 单例初始化,零配置也能跑。
6
7
  process.on('exit', () => exitAltScreen());
7
8
  process.on('SIGINT', () => {
8
9
  exitAltScreen();
package/dist/llm/index.js CHANGED
@@ -2,10 +2,22 @@ import OpenAI from 'openai';
2
2
  import { config } from '../config/index.js';
3
3
  import { tools } from '../tools/registry.js';
4
4
  import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
5
- const client = new OpenAI({
5
+ let client = new OpenAI({
6
6
  baseURL: config.baseURL,
7
7
  apiKey: config.apiKey,
8
8
  });
9
+ /**
10
+ * 运行时重建 OpenAI 客户端(/model 切换 baseURL/apiKey 后调)。
11
+ * config.model 已在 chat() 每次读取(热切),但 client 的 baseURL/apiKey 是构造时固化的实例字段,
12
+ * 改 config 后必须重建 client 才能让新 baseURL/apiKey 对后续请求生效。
13
+ * 子 agent 复用本模块 chat(),故只此一处重建即全链路生效。
14
+ */
15
+ export function reconfigureClient() {
16
+ client = new OpenAI({
17
+ baseURL: config.baseURL,
18
+ apiKey: config.apiKey,
19
+ });
20
+ }
9
21
  /** 把内部工具定义转成 OpenAI 的 tool 格式 */
10
22
  export const chatTools = tools.map((t) => ({
11
23
  type: 'function',
@@ -1,8 +1,8 @@
1
1
  import readline from 'node:readline/promises';
2
2
  import { emitKeypressEvents } from 'node:readline';
3
3
  import { stdin, stdout } from 'node:process';
4
- import { config, PLAN_MODE_SUFFIX } from '../config/index.js';
5
- import { updateConfigKey } from '../config/file.js';
4
+ import { config, PLAN_MODE_SUFFIX, updateModelConfig, isModelConfigured } from '../config/index.js';
5
+ import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
6
6
  import { runAgent } from '../agent/index.js';
7
7
  import { getAgentMode, setAgentMode, onModeChange } from '../agent/mode.js';
8
8
  import { setSandboxRoot } from '../sandbox/root.js';
@@ -13,7 +13,7 @@ import * as mouse from '../ui/mouse.js';
13
13
  import { promptWithSlashMenu, promptTurnPicker, promptSessionPicker, promptThemePicker, promptRevertChoice, } from '../ui/prompt.js';
14
14
  import { promptIntervention } from '../ui/intervention.js';
15
15
  import { tools } from '../tools/registry.js';
16
- import { estimateMessagesTokens, } from '../llm/index.js';
16
+ import { estimateMessagesTokens, reconfigureClient, } from '../llm/index.js';
17
17
  import { compactHistory, contextState, newSessionId, saveSession, loadSession, listSessions, } from '../session/index.js';
18
18
  import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, } from '../rollback/index.js';
19
19
  import { listSkills, effectiveSystemPrompt } from '../skills/index.js';
@@ -36,6 +36,7 @@ const SLASH_COMMANDS = [
36
36
  { name: '/reflect', desc: '手动触发后台记忆反思 pass' },
37
37
  { name: '/init', desc: '扫描项目生成 MOCODE.md 项目记忆' },
38
38
  { name: '/theme', desc: '切换颜色主题(↑↓·Enter)' },
39
+ { name: '/model', desc: '配置大模型(baseURL/key/model/窗口)' },
39
40
  { name: '/plan', desc: '切到 plan 模式(只读探查+产出计划)' },
40
41
  { name: '/auto', desc: '切回 auto 模式(全工具执行)' },
41
42
  ];
@@ -47,6 +48,23 @@ const THEME_DESCRIPTIONS = {
47
48
  gruvbox: 'Gruvbox 暖色',
48
49
  nord: 'Nord 冷色',
49
50
  };
51
+ /** /model 预设后端:选一个预填 baseURL,仍可逐项改。base_url 取自 README 常见表。 */
52
+ const MODEL_PRESETS = [
53
+ { label: 'GLM(智谱)', baseURL: 'https://open.bigmodel.cn/api/v3', model: 'glm-4.6', window: 128000 },
54
+ { label: 'DeepSeek', baseURL: 'https://api.deepseek.com', model: 'deepseek-chat', window: 64000 },
55
+ { label: 'Qwen(阿里)', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus', window: 128000 },
56
+ { label: '本地 Ollama', baseURL: 'http://localhost:11434/v1', model: 'qwen2.5:7b', window: 32768 },
57
+ { label: '本地 vLLM', baseURL: 'http://localhost:8000/v1', model: 'default', window: 32768 },
58
+ { label: '自定义 base_url', baseURL: '', model: '', window: 128000 },
59
+ ];
60
+ /** apiKey 脱敏:只露末 4 位,前面打星号(显示用,绝不把明文 key 写进内容区)。 */
61
+ function maskKey(k) {
62
+ if (!k)
63
+ return '(未设置)';
64
+ if (k.length <= 8)
65
+ return '****';
66
+ return `${'='.repeat(Math.min(k.length - 4, 20))}${k.slice(-4)}`;
67
+ }
50
68
  /**
51
69
  * /init 指令:发给 agent 扫描项目并生成 MOCODE.md(对标 Claude Code /init 生成 CLAUDE.md,
52
70
  * 但 mocode 读 MOCODE.md)。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
@@ -144,6 +162,8 @@ function runningStateFor(cmd) {
144
162
  return { status: '清空', placeholder: '…' };
145
163
  case '/theme':
146
164
  return { status: '切主题', placeholder: '选择主题…' };
165
+ case '/model':
166
+ return { status: '配模型', placeholder: '配置中…' };
147
167
  default:
148
168
  // 输入框留空(运行中可 typeahead 打字,dim 回显);运行状态由内联 spinner 承载(思考中/执行…),
149
169
  // 状态行只显走时——故常态 status 留空,不塞「处理」这种与内联重复的泛标签。
@@ -396,6 +416,10 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
396
416
  // 自更新提示:开场静态段(进 INPUT 态前),dim 一行,不与流式 / 输入争用。
397
417
  layout.contentWrite(` ${ui.gray}↳ ${updateNotice}${ui.reset}\n`);
398
418
  }
419
+ if (!isModelConfigured()) {
420
+ // 未配置 baseURL/apiKey:醒目提示引导 /model(不退出,REPL 仍可用;发消息会失败但不崩)。
421
+ layout.contentWrite(`${ui.yellow} ⚠ 未配置大模型。输入 ${ui.cyan}/model${ui.yellow} 配置 baseURL / apiKey / model(即时生效),或退出后运行 ${ui.cyan}mocode config${ui.yellow} 走向导。${ui.reset}\n`);
422
+ }
399
423
  layout.contentWrite(`${ui.dim} /plan · /auto · Shift+Tab 切换模式(plan:只读探查 + 产出计划,审批后切 auto 执行)${ui.reset}\n`);
400
424
  /**
401
425
  * 切换 agent 模式(Shift+Tab 触发,经 prompt.ts 的 onCycleMode 回调)。
@@ -772,6 +796,179 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
772
796
  }
773
797
  continue;
774
798
  }
799
+ if (line === '/model' || line.startsWith('/model ')) {
800
+ // /model:运行时配置大模型(baseURL/apiKey/model/contextWindowTokens)。
801
+ // 即时生效(updateModelConfig 改内存 + env,reconfigureClient 重建 OpenAI 实例)+ 持久化(writeConfigKeys 写 ~/.mocode/config)。
802
+ // 仿 /theme:promptIntervention 弹菜单/输入 → 改 config → refreshStatusBase 刷底栏 → clearContent+banner 重显横幅 → dim 警告(shell env 覆盖)。
803
+ const arg = line.startsWith('/model ') ? line.slice('/model '.length).trim() : '';
804
+ // /model list:显示当前四项配置(apiKey 脱敏)。
805
+ if (arg === 'list' || arg === 'show') {
806
+ layout.contentWrite(`${ui.dim}当前模型配置:${ui.reset}\n`);
807
+ layout.contentWrite(` ${ui.cyan}baseURL${ui.reset} ${config.baseURL}\n`);
808
+ layout.contentWrite(` ${ui.cyan}apiKey ${ui.reset} ${maskKey(config.apiKey)}\n`);
809
+ layout.contentWrite(` ${ui.cyan}model ${ui.reset} ${config.model}\n`);
810
+ layout.contentWrite(` ${ui.cyan}窗口 ${ui.reset} ${config.contextWindowTokens} tokens\n`);
811
+ layout.contentWrite(`${ui.dim}(配置文件: ${CONFIG_PATH})${ui.reset}\n`);
812
+ continue;
813
+ }
814
+ // 1) 选 provider 预设(预填 baseURL,后续仍可逐项改)。
815
+ let preset;
816
+ try {
817
+ const res = await promptIntervention({
818
+ type: 'choice',
819
+ title: '选择后端预设(预填 baseURL,后续可改)',
820
+ detail: '选一个会预填 baseURL/model/窗口,之后逐项确认。选「自定义」全部手填。',
821
+ options: MODEL_PRESETS.map((p) => p.label),
822
+ });
823
+ if (res.action === 'cancelled') {
824
+ continue;
825
+ }
826
+ const idx = MODEL_PRESETS.findIndex((p) => p.label === res.value);
827
+ if (idx === -1) {
828
+ continue;
829
+ }
830
+ preset = MODEL_PRESETS[idx];
831
+ }
832
+ catch {
833
+ continue; // Ctrl+C
834
+ }
835
+ // 1.5) 一键应用确认:非「自定义」预设(带预填值)给直接应用入口,免连按 4 次回车。
836
+ // 直接应用 = 用预设 model/baseURL/window + 保留当前 apiKey(等价于下方逐项链连按回车)。
837
+ // 逐项修改 / 自定义输入(promptIntervention choice 自动追加的「其他」项 submitted)→ 回落 4 步链。
838
+ // 「自定义」预设字段空,跳过确认直接进链。
839
+ let quickApply = false;
840
+ if (preset.model || preset.baseURL) {
841
+ try {
842
+ const res = await promptIntervention({
843
+ type: 'choice',
844
+ title: `应用 ${preset.label}?`,
845
+ detail: `model ${preset.model}\nbaseURL ${preset.baseURL}\napiKey ${maskKey(config.apiKey)}(直接应用=保留当前)\n窗口 ${preset.window}`,
846
+ options: ['直接应用', '逐项修改'],
847
+ });
848
+ if (res.action === 'cancelled') {
849
+ continue;
850
+ }
851
+ if (res.action === 'selected' && res.value === '直接应用') {
852
+ quickApply = true;
853
+ }
854
+ // 其余(逐项修改 / 自定义输入 submitted)→ quickApply 保持 false,走下方逐项链
855
+ }
856
+ catch {
857
+ continue; // Ctrl+C
858
+ }
859
+ }
860
+ // 2) 收集 baseURL / apiKey / model / contextWindowTokens。
861
+ // quickApply:直接取预设值 + 当前 apiKey;否则逐项 input(预填 preset 值,回车=采纳;apiKey 不预填明文,回车=保留旧值)。
862
+ let baseURL;
863
+ let apiKey;
864
+ let model;
865
+ let window;
866
+ if (quickApply) {
867
+ baseURL = preset.baseURL;
868
+ apiKey = config.apiKey;
869
+ model = preset.model;
870
+ window = preset.window;
871
+ }
872
+ else {
873
+ // baseURL
874
+ {
875
+ const res = await promptIntervention({
876
+ type: 'input',
877
+ title: 'LLM_BASE_URL',
878
+ detail: 'OpenAI 兼容 API 端点。回车采纳预填值。',
879
+ seed: preset.baseURL || config.baseURL,
880
+ });
881
+ if (res.action === 'cancelled') {
882
+ continue;
883
+ }
884
+ baseURL = (res.value ?? '').trim() || preset.baseURL || config.baseURL;
885
+ }
886
+ if (!baseURL) {
887
+ layout.contentWrite(`${ui.yellow}baseURL 不能为空,已取消。${ui.reset}\n`);
888
+ continue;
889
+ }
890
+ // apiKey(不预填明文:回车=保留旧值,输入新值=覆盖)
891
+ {
892
+ const res = await promptIntervention({
893
+ type: 'input',
894
+ title: 'LLM_API_KEY',
895
+ detail: `回车保留当前 ${maskKey(config.apiKey)};输入新值则覆盖。`,
896
+ seed: '',
897
+ });
898
+ if (res.action === 'cancelled') {
899
+ continue;
900
+ }
901
+ const v = (res.value ?? '').trim();
902
+ apiKey = v || config.apiKey;
903
+ }
904
+ if (!apiKey) {
905
+ layout.contentWrite(`${ui.yellow}apiKey 不能为空,已取消。${ui.reset}\n`);
906
+ continue;
907
+ }
908
+ // model
909
+ {
910
+ const res = await promptIntervention({
911
+ type: 'input',
912
+ title: 'LLM_MODEL',
913
+ detail: '模型名(须支持 function calling)。回车采纳预填值。',
914
+ seed: preset.model || config.model,
915
+ });
916
+ if (res.action === 'cancelled') {
917
+ continue;
918
+ }
919
+ model = (res.value ?? '').trim() || preset.model || config.model;
920
+ }
921
+ if (!model) {
922
+ layout.contentWrite(`${ui.yellow}model 不能为空,已取消。${ui.reset}\n`);
923
+ continue;
924
+ }
925
+ // contextWindowTokens
926
+ {
927
+ const res = await promptIntervention({
928
+ type: 'input',
929
+ title: 'CONTEXT_WINDOW_TOKENS',
930
+ detail: '模型上下文窗口(须对齐真实模型;GLM≈128k,DeepSeek-V3≈64k)。回车采纳预填值。',
931
+ seed: String(preset.window || config.contextWindowTokens),
932
+ });
933
+ if (res.action === 'cancelled') {
934
+ continue;
935
+ }
936
+ const v = (res.value ?? '').trim();
937
+ const n = Number(v);
938
+ if (!v || !Number.isFinite(n) || n <= 0) {
939
+ // 非法输入:保留旧值,不阻断(用 preset.window 或当前值兜底)
940
+ window = preset.window || config.contextWindowTokens;
941
+ }
942
+ else {
943
+ window = Math.floor(n);
944
+ }
945
+ }
946
+ }
947
+ // 3) 应用:内存 config + env(updateModelConfig)→ 持久化(writeConfigKeys)→ 重建 client(reconfigureClient)。
948
+ updateModelConfig({ model, baseURL, apiKey, contextWindowTokens: window });
949
+ writeConfigKeys({
950
+ LLM_BASE_URL: baseURL,
951
+ LLM_API_KEY: apiKey,
952
+ LLM_MODEL: model,
953
+ CONTEXT_WINDOW_TOKENS: String(window),
954
+ });
955
+ reconfigureClient();
956
+ // 4) 刷新 UI:底栏模型名 + 重显横幅(banner() 闭包实时读 config,自动反映新值)。
957
+ refreshStatusBase(history);
958
+ layout.clearContent();
959
+ if (history.some((m) => m.role === 'user')) {
960
+ renderHistory(history);
961
+ }
962
+ else {
963
+ layout.contentWrite(bannerString(banner()));
964
+ }
965
+ layout.contentWrite(`${ui.dim}(已切换模型 → ${model} @ ${baseURL})${ui.reset}\n`);
966
+ // 5) dim 警告:shell export 的 LLM 键下次启动会覆盖文件值。
967
+ if (config.llmKeysFromShell.length > 0) {
968
+ layout.contentWrite(`${ui.dim}(shell 环境变量已设 ${config.llmKeysFromShell.join(' / ')},文件写入下次启动被其覆盖;取消该 shell 设置后生效)${ui.reset}\n`);
969
+ }
970
+ continue;
971
+ }
775
972
  if (line === '/rollback' || line.startsWith('/rollback ')) {
776
973
  // /rollback:打开轮次菜单(↑/↓ 选,Enter 回滚到该轮并预填其输入,再 Enter 重新跑)。
777
974
  // 忽略任何数字参数(原「输数字选回滚」已删,统一走菜单)。无快照的旧轮次(/resume 重建)文件改动不可撤销。
@@ -0,0 +1,135 @@
1
+ // 运行中上下文剔除(drop_context 工具的核心):把历史里无关的 tool 结果替换为存根。
2
+ //
3
+ // 与 compact 的区别:compact 是阈值触发的整体压缩(微截 + 摘要),drop_context 是 agent 主动、
4
+ // 精准剔除"已判定无关"的具体 tool 结果——agent 检索到大量无关信息后主动调用,释放上下文。
5
+ //
6
+ // 不变量(对齐 compact.ts):
7
+ // - 永不动 history[0](system prompt)。
8
+ // - 永不动当前轮:从末尾向前找到最后一个 user 消息,该 user 及其之后的 tool 结果一律保留
9
+ // (agent 本轮还在用,踢了会丢失正在进行的上下文)。
10
+ // - tool_call_id 配对:只改 tool 消息的 content,不删消息、不动 tool_calls 数组结构、不改 id。
11
+ // - 原地修改 history(同 compact:length=0;push 重建,repl 持有同一引用)。
12
+ // - 永不抛错(对齐「调度器永不抛错」契约);无匹配 / 无可剔除 → 返 dropped=0。
13
+ import { messageTokens, estimateTokens, } from '../llm/index.js';
14
+ /** 把消息 content 拍平成字符串(OpenAI 可能 string / null / 多模态数组)。 */
15
+ function toText(content) {
16
+ if (content == null)
17
+ return '';
18
+ if (typeof content === 'string')
19
+ return content;
20
+ try {
21
+ return JSON.stringify(content);
22
+ }
23
+ catch {
24
+ return String(content);
25
+ }
26
+ }
27
+ /**
28
+ * 从 history 末尾向前找最后一个 user 消息的索引;无 user 返 -1。
29
+ * 用于划定"当前轮保护区"——该 user 及其之后的消息一律不剔除。
30
+ */
31
+ function lastUserIndex(history) {
32
+ for (let i = history.length - 1; i >= 1; i--) {
33
+ if (history[i].role === 'user')
34
+ return i;
35
+ }
36
+ return -1;
37
+ }
38
+ /** 取 tool 消息对应的工具名(从紧邻的前导 assistant.tool_calls 按 tool_call_id 配对找)。 */
39
+ function toolNameOf(history, idx) {
40
+ const tcId = history[idx].tool_call_id;
41
+ if (!tcId)
42
+ return null;
43
+ for (let j = idx - 1; j >= 1; j--) {
44
+ const m = history[j];
45
+ if (m.role !== 'assistant')
46
+ continue;
47
+ const tcs = m
48
+ .tool_calls;
49
+ if (!tcs)
50
+ continue;
51
+ const hit = tcs.find((tc) => tc?.id === tcId);
52
+ if (hit)
53
+ return hit.function?.name ?? null;
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * 剔除历史里命中的旧 tool 结果(原地修改 history)。
59
+ *
60
+ * 筛选(各维度 AND 组合):
61
+ * - toolNames:只剔除这些工具名的结果(空 = 不限)
62
+ * - contains:只剔除内容包含所有这些词(AND、大小写不敏感)的结果(空 = 不限)
63
+ *
64
+ * 保护:history[0](system)+ 当前轮(最后一个 user 及其之后)永不剔除。
65
+ * 已是存根的 tool 消息(含「已剔除」标记)不重复剔除(幂等)。
66
+ *
67
+ * 永不抛错;无匹配返 dropped=0。
68
+ */
69
+ export function dropContextFromHistory(history, filter) {
70
+ const toolNames = filter.toolNames && filter.toolNames.length > 0
71
+ ? new Set(filter.toolNames)
72
+ : null;
73
+ const contains = filter.contains && filter.contains.length > 0
74
+ ? filter.contains.map((s) => s.toLowerCase())
75
+ : null;
76
+ // 当前轮保护区:最后一个 user 及其之后一律保留(agent 还在用)。
77
+ const guard = lastUserIndex(history);
78
+ // guard <= 0 表示无 user 或 user 就是 history[0](不会):整段历史都可剔除(除 history[0])。
79
+ const protectedFrom = guard > 0 ? guard : 0; // < protectedFrom 的才可剔除(即 [1, protectedFrom)
80
+ const items = [];
81
+ let freedTokens = 0;
82
+ const STUB_PREFIX = '⌦[已剔除:与当前任务无关]';
83
+ for (let i = 1; i < protectedFrom; i++) {
84
+ const m = history[i];
85
+ if (m.role !== 'tool')
86
+ continue;
87
+ const content = toText(m.content);
88
+ // 幂等:已是存根(含标记)不重复剔除。
89
+ if (content.startsWith(STUB_PREFIX))
90
+ continue;
91
+ // 维度 1:工具名
92
+ const tname = toolNameOf(history, i);
93
+ if (toolNames && (!tname || !toolNames.has(tname)))
94
+ continue;
95
+ // 维度 2:内容关键词(AND)
96
+ if (contains) {
97
+ const lower = content.toLowerCase();
98
+ if (!contains.every((kw) => lower.includes(kw)))
99
+ continue;
100
+ }
101
+ // 命中 → 替换为存根(保 tool_call_id 不动,只改 content)
102
+ const before = messageTokens(m);
103
+ const id = m.tool_call_id ?? '';
104
+ const stub = `${STUB_PREFIX} 原 ${tname ?? 'tool'} 结果(${content.length} 字符,约 ${before} tokens)${id ? ` · id …${id.slice(-6)}` : ''}⌫`;
105
+ m.content = stub;
106
+ const after = messageTokens(m);
107
+ freedTokens += Math.max(0, before - after);
108
+ items.push({
109
+ toolName: tname ?? 'tool',
110
+ toolCallId: id.slice(-6),
111
+ });
112
+ }
113
+ return {
114
+ dropped: items.length,
115
+ freedTokens,
116
+ items,
117
+ };
118
+ }
119
+ /** 给 drop_context 工具结果格式化人类可读摘要(回灌给 agent)。 */
120
+ export function formatDropResult(r) {
121
+ if (r.dropped === 0) {
122
+ return '未剔除任何工具结果(无匹配的旧 tool 消息,或均在当前轮保护区内不可剔除)。';
123
+ }
124
+ const lines = [
125
+ `已剔除 ${r.dropped} 条无关工具结果,释放约 ${r.freedTokens} tokens。`,
126
+ '被剔除项(已替换为存根,tool_call_id 配对不变):',
127
+ ];
128
+ for (const it of r.items) {
129
+ lines.push(` - ${it.toolName} (id …${it.toolCallId})`);
130
+ }
131
+ lines.push('这些结果在后续上下文中仅保留存根标记,不再占用篇幅。');
132
+ return lines.join('\n');
133
+ }
134
+ // 供 drop_context 工具估算用(避免直接 import llm 的公开 API 造成耦合,这里重导出)。
135
+ export { estimateTokens };
@@ -5,4 +5,5 @@
5
5
  * 依赖方向:session → {llm(摘要复用 chat), config, ui};llm 不反向依赖 session。
6
6
  */
7
7
  export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, contextState, } from './compact.js';
8
+ export { dropContextFromHistory, formatDropResult, } from './drop.js';
8
9
  export { newSessionId, saveSession, loadSession, listSessions, sessionDir, } from './persist.js';
@@ -3,10 +3,10 @@ import { promptIntervention } from '../../ui/intervention.js';
3
3
  export const askHumanTool = {
4
4
  name: 'ask_human',
5
5
  description: [
6
- 'Call this tool when you hit a decision point during a task that requires human input — it pops up a question panel in the terminal for the user to choose.',
7
- 'Use when: multiple implementation approaches need a user decision, user intent is unclear and needs clarification, or extra info is needed to proceed.',
8
- 'Blocks until the user responds; the user can pick a preset option or choose "custom input" to answer freely; the result is returned as the tool result.',
9
- 'Do not call frequently when the task is clear and you can decide yourself — it interrupts the user. When options is omitted or empty, it becomes free-text input instead.',
6
+ 'Ask the user for input at a decision point (blocks until they respond).',
7
+ ' Use when: multiple approaches need a user decision, intent is unclear, or extra info is needed.',
8
+ ' Don\'t call when the task is clear and you can decide it interrupts the user.',
9
+ ' Options (2-6) let the user pick; omit/empty for free-text input. Their answer is returned as the result.',
10
10
  ].join(''),
11
11
  parameters: {
12
12
  type: 'object',
@@ -50,9 +50,9 @@ function runCodegraph(args) {
50
50
  }
51
51
  export const codegraphTool = {
52
52
  name: 'codegraph',
53
- description: 'Preferred tool for understanding/locating code, tracing call chains, and assessing the impact of changes in repos with a code index (.codegraph/).' +
54
- ' Returns relevant symbol source + call paths in one shot — more accurate and economical than piecing together via read_file/grep.' +
55
- ' Returns a hint when .codegraph/ is absent (build it first with `codegraph init`).',
53
+ description: 'Preferred for understanding/locating code, tracing call chains, assessing impact of changes in repos with a code index (.codegraph/).' +
54
+ ' Returns symbol source + call paths in one shot — more accurate/economical than read_file/grep.' +
55
+ ' Hints to run `codegraph init` when .codegraph/ is absent.',
56
56
  parameters: {
57
57
  type: 'object',
58
58
  properties: {
@@ -0,0 +1,68 @@
1
+ // ---------- drop_context ----------
2
+ /**
3
+ * 运行中上下文剔除工具:agent 检索到大量无关信息后,主动把历史里无关的 tool 结果替换为存根,
4
+ * 释放上下文空间(保 tool_call_id 配对不变量,只改 content)。
5
+ *
6
+ * 与 compact 的区别:compact 是阈值触发的整体压缩(微截 + 摘要);drop_context 是 agent 主动、
7
+ * 精准剔除"已判定无关"的具体 tool 结果。
8
+ *
9
+ * 保护:history[0](system)与当前轮(最后一个 user 及其之后)永不剔除——agent 还在用。
10
+ * 已是存根的不重复剔除(幂等)。永不抛错。
11
+ *
12
+ * 筛选(各维度 AND 组合,全部可选;不传 = 剔除所有可剔除的旧 tool 结果):
13
+ * - toolNames:只剔除这些工具名的结果(如 ["grep","read_file"])
14
+ * - contains:只剔除内容包含所有这些词(AND、大小写不敏感)的结果
15
+ *
16
+ * plan 模式不禁用:纯上下文管理,无文件 / 命令副作用。
17
+ */
18
+ export const dropContextTool = {
19
+ name: 'drop_context',
20
+ description: [
21
+ 'Drop (stub-replace) irrelevant OLDER tool results from history to free context.',
22
+ 'COST-AWARE: ~300-token round-trip; only call if freed tokens clearly exceed it — i.e. MULTIPLE bulky results (e.g. a wide grep/read sweep of mostly-irrelevant hits), not a single small one or near done.',
23
+ 'Never dropped: system prompt and the CURRENT turn (last user message onward). Idempotent. Filters AND-combine; omit both = drop all droppable. Returns dropped count, freed tokens, tool names.',
24
+ ].join(' '),
25
+ parameters: {
26
+ type: 'object',
27
+ properties: {
28
+ toolNames: {
29
+ type: 'array',
30
+ items: { type: 'string' },
31
+ description: 'Only drop results from these tool names (e.g. ["grep","read_file"]). Empty/omitted = no tool-name filter.',
32
+ },
33
+ contains: {
34
+ type: 'array',
35
+ items: { type: 'string' },
36
+ description: 'Only drop results whose content contains ALL of these keywords (AND, case-insensitive). Empty/omitted = no content filter.',
37
+ },
38
+ },
39
+ required: [],
40
+ },
41
+ async execute(args, ctx) {
42
+ const dropContext = ctx?.dropContext;
43
+ if (!dropContext) {
44
+ // 无注入(理论上不会:runAgentCore 总注入)。降级:不改 history,告知 agent。
45
+ return '错误:上下文剔除回调不可用(未由 agent 循环注入),无法剔除。';
46
+ }
47
+ const filter = {};
48
+ if (Array.isArray(args.toolNames)) {
49
+ filter.toolNames = args.toolNames
50
+ .filter((v) => typeof v === 'string' && v.length > 0)
51
+ .map((v) => String(v));
52
+ }
53
+ if (Array.isArray(args.contains)) {
54
+ filter.contains = args.contains
55
+ .filter((v) => typeof v === 'string' && v.length > 0)
56
+ .map((v) => String(v));
57
+ }
58
+ const result = dropContext(filter);
59
+ return result.dropped === 0
60
+ ? '未剔除任何工具结果(无匹配的旧 tool 消息,或均在当前轮保护区内不可剔除)。'
61
+ : [
62
+ `已剔除 ${result.dropped} 条无关工具结果,释放约 ${result.freedTokens} tokens。`,
63
+ '被剔除项(已替换为存根,tool_call_id 配对不变):',
64
+ ...result.items.map((it) => ` - ${it.toolName} (id …${it.toolCallId})`),
65
+ '这些结果在后续上下文中仅保留存根标记,不再占用篇幅。',
66
+ ].join('\n');
67
+ },
68
+ };
@@ -3,7 +3,7 @@ import { resolve } from 'node:path';
3
3
  // ---------- edit_file ----------
4
4
  export const editFileTool = {
5
5
  name: 'edit_file',
6
- description: 'Make a precise string replacement in a file. old_string must occur exactly once in the file and match exactly (including indentation/newlines). Use write_file for new files.',
6
+ description: 'Replace a string in a file. old_string must occur exactly once and match exactly (including indentation/newlines). Use write_file for new files.',
7
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
@@ -4,8 +4,8 @@ import { getSandboxRoot, isInsideRoot } from '../../sandbox/index.js';
4
4
  // ---------- glob ----------
5
5
  export const globTool = {
6
6
  name: 'glob',
7
- description: 'Find file paths matching a glob pattern (e.g. **/*.ts). Returns a list of matches (auto-excludes node_modules / .git).' +
8
- ' Note: when understanding code architecture/call chains, if a .codegraph/ index exists, prefer the codegraph tool over piecing together via glob one file at a time.',
7
+ description: 'Find files matching a glob pattern (e.g. **/*.ts). Auto-excludes node_modules/.git.' +
8
+ ' For architecture/call chains, prefer codegraph.',
9
9
  parameters: {
10
10
  type: 'object',
11
11
  properties: {
@@ -5,8 +5,8 @@ import { getSandboxRoot, isInsideRoot, jailResolve } from '../../sandbox/index.j
5
5
  // ---------- grep ----------
6
6
  export const grepTool = {
7
7
  name: 'grep',
8
- description: 'Search file contents by regex, returning file:line: matched lines. Recursively searches the current directory by default (excluding node_modules/.git). Optional glob to restrict file types.' +
9
- ' Note: when understanding code architecture/call chains, if a .codegraph/ index exists, prefer the codegraph tool over piecing together via grep one file at a time.',
8
+ description: 'Search file contents by regex, returning file:line: matched lines. Recursively searches cwd excluding node_modules/.git. Optional glob restricts file types.' +
9
+ ' For architecture/call chains, prefer codegraph.',
10
10
  parameters: {
11
11
  type: 'object',
12
12
  properties: {
@@ -10,6 +10,7 @@ import { useSkillTool } from './use-skill.js';
10
10
  import { askHumanTool } from './ask-human.js';
11
11
  import { codegraphTool } from './codegraph.js';
12
12
  import { switchModeTool } from './switch-mode.js';
13
+ import { dropContextTool } from './drop-context.js';
13
14
  import { memorySaveTool } from './memory-save.js';
14
15
  import { memorySearchTool } from './memory-search.js';
15
16
  import { memoryListTool } from './memory-list.js';
@@ -33,6 +34,7 @@ export const builtinTools = [
33
34
  useSkillTool,
34
35
  askHumanTool,
35
36
  switchModeTool, // plan↔auto 自切(两模式都可见,不进 PLAN_DISABLED_TOOLS;副作用控制工具→串行分支)
37
+ dropContextTool, // 运行中剔除无关 tool 结果(上下文管理,无副作用;两模式都可见,串行分支)
36
38
  memorySaveTool,
37
39
  memorySearchTool,
38
40
  memoryListTool,
@@ -3,7 +3,7 @@ import { forgetEntry } from '../../memory/store.js';
3
3
  // 遗忘:默认归档(archived,从索引/默认搜索隐藏,可复活);mode=delete 硬删。pinned 拒删。
4
4
  export const memoryForgetTool = {
5
5
  name: 'memory_forget',
6
- description: 'Forget a memory entry: by default archives (archived, hidden from index and default search, can be revived via memory_update); mode=delete hard-deletes. Pinned entries cannot be deleted (first memory_update pinned=false to unpin).',
6
+ description: 'Forget a memory entry: default archive (hidden, revivable via memory_update); mode=delete hard-deletes. Pinned entries can\'t be deleted (unpin first).',
7
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
@@ -3,7 +3,7 @@ import { listEntries } from '../../memory/store.js';
3
3
  // 列索引(id/name/summary,无正文、不 bump recall)。用于浏览有哪些、拿 id 再 memory_search 取正文。
4
4
  export const memoryListTool = {
5
5
  name: 'memory_list',
6
- description: 'List the memory index (id/name/summary, no body). Defaults to active. Get an id, then use memory_search to retrieve the full body.',
6
+ description: 'List the memory index (id/name/summary, no body). Get an id, then use memory_search for the full body.',
7
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
@@ -4,7 +4,7 @@ import { saveEntry } from '../../memory/store.js';
4
4
  // 撞库(name→id 已存在)拒绝,引导用 memory_update。
5
5
  export const memorySaveTool = {
6
6
  name: 'memory_save',
7
- description: 'Save a long-term memory entry (cross-session). Store only non-obvious, long-term-useful facts/decisions/pitfalls. The title goes into the startup index; retrieve full body on demand via memory_search.',
7
+ description: 'Save a cross-session long-term memory entry. Store only non-obvious, useful facts/decisions/pitfalls. Title enters the startup index; retrieve body via memory_search.',
8
8
  parameters: {
9
9
  type: 'object',
10
10
  properties: {
@@ -4,7 +4,7 @@ import { searchEntries } from '../../memory/store.js';
4
4
  // 结果走 capToolResultForHistory 的放宽上限(同 use_skill,保正文完整)。
5
5
  export const memorySearchTool = {
6
6
  name: 'memory_search',
7
- description: 'Search memory entries by keyword (multi-word substring match, ranked by relevance), returning full body of matching entries. A hit bumps recall count (affects forgetting decay).',
7
+ description: 'Search memory entries by keyword (substring match), returning full body.',
8
8
  parameters: {
9
9
  type: 'object',
10
10
  properties: {
@@ -3,7 +3,7 @@ import { updateEntry } from '../../memory/store.js';
3
3
  // 原地改一条记忆(id 不变)。反思的弱意义:干活时发现事实变了/过时即纠正。
4
4
  export const memoryUpdateTool = {
5
5
  name: 'memory_update',
6
- description: 'Update a memory entry in place (id unchanged). Use when facts changed / correcting outdated info / updating summary or body / toggling pinned. Get id from memory_list or memory_search.',
6
+ description: 'Update a memory entry in place (id unchanged). Use when facts changed / correcting outdated info / toggling pinned. Get id from memory_list or memory_search.',
7
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
@@ -4,8 +4,8 @@ import { MAX_FILE_LINES } from '../constants.js';
4
4
  // ---------- read_file ----------
5
5
  export const readFileTool = {
6
6
  name: 'read_file',
7
- description: 'Read file content, returning text with line numbers. Read before editing code. Optional offset (start line, 1-based, default 1) and limit (number of lines, default 2000).' +
8
- ' Note: when understanding code architecture/call chains, if a .codegraph/ index exists, prefer the codegraph tool over piecing together via read_file one file at a time.',
7
+ description: 'Read file content with line numbers. Read before editing. offset (1-based, default 1), limit (default 2000).' +
8
+ ' For architecture/call chains, prefer codegraph over reading files one at a time.',
9
9
  parameters: {
10
10
  type: 'object',
11
11
  properties: {
@@ -4,7 +4,7 @@ import { getSandboxRoot, filterEnv, isCommandDenied } from '../../sandbox/index.
4
4
  // ---------- run_command ----------
5
5
  export const runCommandTool = {
6
6
  name: 'run_command',
7
- description: 'Execute a shell command, returning combined stdout+stderr. Default timeout 120s. Use for running tests, builds, git, etc.',
7
+ description: 'Run a shell command, merging stdout+stderr. Default timeout 120s. For tests, builds, git, etc.',
8
8
  parameters: {
9
9
  type: 'object',
10
10
  properties: {
@@ -3,11 +3,9 @@ import { setAgentMode, getAgentMode } from '../../agent/mode.js';
3
3
  export const switchModeTool = {
4
4
  name: 'switch_mode',
5
5
  description: [
6
- 'Switch the agent mode between "plan" (read-only investigation + planning) and "auto" (full tool execution).',
7
- 'In plan mode, write_file / edit_file / run_command / memory_save / memory_update / memory_forget are disabled; in auto mode, all tools are available.',
8
- 'Use this to autonomously transition from planning to execution WITHIN THE SAME TURN: investigate in plan mode, produce a plan, then call switch_mode("auto") and continue implementing it in the same turn ONLY when the user has explicitly asked you to "plan first then execute" / "先 plan 再 auto" / autonomous execution.',
9
- 'Do NOT switch to auto if the user entered plan mode manually (via /plan or Shift+Tab) for a safety review — in that case present the plan and STOP; the user will approve via a prompt and execution happens in a follow-up turn.',
10
- 'Switching to plan from auto is rarely needed; do it only if you realize you should investigate before changing anything.',
6
+ 'Switch agent mode between "plan" (read-only investigation) and "auto" (full tool execution).',
7
+ 'Use to transition from planning to execution WITHIN THE SAME TURN: in plan, after presenting a plan, call switch_mode("auto") and continue ONLY when the user asked for autonomous execution ("先 plan 再 auto" etc.).',
8
+ 'If the user entered plan mode manually (/plan or Shift+Tab) for review, do NOT switch to autopresent the plan and STOP for approval.',
11
9
  ].join(' '),
12
10
  parameters: {
13
11
  type: 'object',
@@ -9,10 +9,9 @@ import { MAX_OUTPUT } from '../constants.js';
9
9
  export const taskTool = {
10
10
  name: 'task',
11
11
  description: [
12
- 'Spawn a sub-agent to handle an isolated sub-task with its own conversation history (independent of the main thread). The sub-agent runs to completion and returns a concise summary.',
13
- 'Use when: a task can be decomposed into independent sub-tasks, you want to explore multiple files/areas without polluting the main history, or a sub-task involves many tool calls that would bloat the main context window.',
14
- 'The sub-agent has its own history; only its final summary is returned to you as the tool result. It cannot recursively spawn further sub-agents (no "task" tool available to it).',
15
- 'Optionally restrict the sub-agent to a subset of tools (e.g. read-only tools for pure investigation) via the "tools" parameter.',
12
+ 'Spawn a sub-agent for an isolated sub-task (independent history; only its final summary returns to you).',
13
+ 'Use when a task splits into independent parts or its many tool calls would bloat your context.',
14
+ 'Cannot recursively spawn sub-agents.',
16
15
  ].join(''),
17
16
  parameters: {
18
17
  type: 'object',
@@ -24,7 +23,7 @@ export const taskTool = {
24
23
  tools: {
25
24
  type: 'array',
26
25
  items: { type: 'string' },
27
- description: 'Optional whitelist of tool names the sub-agent is allowed to use (e.g. ["read_file","glob","grep","codegraph"] for read-only investigation). Omit to allow all tools.',
26
+ description: 'Optional whitelist of tool names the sub-agent is allowed to use (e.g. ["read_file","glob","grep","codegraph"] for read-only investigation). Omit to allow all tools. If the sub-task needs verification/build/test (running scripts, typecheck, etc.), remember to include "run_command".',
28
27
  },
29
28
  maxSteps: {
30
29
  type: 'number',
@@ -4,7 +4,7 @@ import { getSkillBody } from '../../skills/index.js';
4
4
  // 系统提示里已列出可用 skill 的 name + description(何时用),模型据此决定调用。
5
5
  export const useSkillTool = {
6
6
  name: 'use_skill',
7
- description: 'Load and return the full SKILL.md instructions for a given skill (pass name). See the skill list in the system prompt for when to use each.',
7
+ description: 'Load the full SKILL.md instructions for a given skill. See the skill list in the system prompt for when to use each.',
8
8
  parameters: {
9
9
  type: 'object',
10
10
  properties: {
@@ -4,7 +4,7 @@ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
4
4
  // ---------- web_fetch ----------
5
5
  export const webFetchTool = {
6
6
  name: 'web_fetch',
7
- description: 'Fetch the web page at a given URL and clean it into plain text (strips HTML tags/scripts/styles, keeps the body). Use to read a link from search results, or a specific URL given by the user.',
7
+ description: 'Fetch a URL and clean HTML to body text. Use to read a link from search results or a URL given by the user.',
8
8
  parameters: {
9
9
  type: 'object',
10
10
  properties: {
@@ -6,7 +6,7 @@ const MAX_CONTENT_CHARS = 800;
6
6
  // ---------- web_search ----------
7
7
  export const webSearchTool = {
8
8
  name: 'web_search',
9
- description: 'Search the web (AnySearch). Returns title/url/snippet/body for each result. Optional tag to switch sub-domain capability (see tag param).',
9
+ description: 'Search the web (AnySearch). Returns title/url/snippet/body per result. Optional tag for sub-domain capability.',
10
10
  parameters: {
11
11
  type: 'object',
12
12
  properties: {
@@ -3,7 +3,7 @@ import { resolve, dirname } from 'node:path';
3
3
  // ---------- write_file ----------
4
4
  export const writeFileTool = {
5
5
  name: 'write_file',
6
- description: 'Create or overwrite a file, creating parent directories as needed.',
6
+ description: 'Create or overwrite a file; parent dirs created.',
7
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
@@ -11,6 +11,7 @@ export const tools = builtinTools;
11
11
  * signal 透传给 tool.execute(经 ctx):长任务工具(run_command/web_fetch)abort 即时取消,
12
12
  * 让用户 Ctrl+C 能跟手中断工具执行(而非等命令跑完 / 超时)。
13
13
  * opts.skipRollback:子 agent 逻辑隔离用——跳过 recordMutation,子 agent 改动不进主回滚快照链。
14
+ * opts.dropContext:上下文剔除回调(drop_context 工具用),透传给 tool.execute 经 ctx。
14
15
  */
15
16
  export async function executeTool(name, argsRaw, signal, opts) {
16
17
  const tool = tools.find((t) => t.name === name);
@@ -38,7 +39,11 @@ export async function executeTool(name, argsRaw, signal, opts) {
38
39
  args.path) {
39
40
  recordMutation(args.path);
40
41
  }
41
- return await tool.execute(args, { signal, skipRollback: opts?.skipRollback });
42
+ return await tool.execute(args, {
43
+ signal,
44
+ skipRollback: opts?.skipRollback,
45
+ dropContext: opts?.dropContext,
46
+ });
42
47
  }
43
48
  catch (e) {
44
49
  return `错误:工具 ${name} 执行失败: ${e instanceof Error ? e.message : String(e)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {