mocode-ai 0.1.7 → 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.
Files changed (48) hide show
  1. package/README.md +22 -28
  2. package/dist/agent/core.js +16 -7
  3. package/dist/commands/config.js +1 -1
  4. package/dist/config/file.js +1 -1
  5. package/dist/config/index.js +48 -7
  6. package/dist/context/classifier.js +2 -1
  7. package/dist/context/encoders/_util.js +40 -0
  8. package/dist/context/encoders/code.js +77 -0
  9. package/dist/context/encoders/doc.js +28 -0
  10. package/dist/context/encoders/graph.js +31 -0
  11. package/dist/context/encoders/index.js +24 -2
  12. package/dist/context/encoders/log.js +52 -0
  13. package/dist/context/encoders/memory.js +63 -0
  14. package/dist/context/encoders/search.js +69 -0
  15. package/dist/context/encoders/summary.js +26 -0
  16. package/dist/context/encoders/table.js +64 -0
  17. package/dist/context/encoders/tree.js +86 -0
  18. package/dist/index.js +2 -1
  19. package/dist/llm/index.js +13 -1
  20. package/dist/repl/index.js +211 -14
  21. package/dist/session/compact.js +49 -18
  22. package/dist/session/drop.js +135 -0
  23. package/dist/session/index.js +1 -0
  24. package/dist/tools/builtins/ask-human.js +4 -4
  25. package/dist/tools/builtins/codegraph.js +3 -3
  26. package/dist/tools/builtins/drop-context.js +68 -0
  27. package/dist/tools/builtins/edit-file.js +1 -1
  28. package/dist/tools/builtins/glob.js +2 -2
  29. package/dist/tools/builtins/grep.js +2 -2
  30. package/dist/tools/builtins/index.js +2 -0
  31. package/dist/tools/builtins/memory-forget.js +1 -1
  32. package/dist/tools/builtins/memory-list.js +1 -1
  33. package/dist/tools/builtins/memory-save.js +1 -1
  34. package/dist/tools/builtins/memory-search.js +1 -1
  35. package/dist/tools/builtins/memory-update.js +1 -1
  36. package/dist/tools/builtins/read-file.js +2 -2
  37. package/dist/tools/builtins/run-command.js +1 -1
  38. package/dist/tools/builtins/switch-mode.js +3 -5
  39. package/dist/tools/builtins/task.js +4 -5
  40. package/dist/tools/builtins/use-skill.js +1 -1
  41. package/dist/tools/builtins/web-fetch.js +1 -1
  42. package/dist/tools/builtins/web-search.js +1 -1
  43. package/dist/tools/builtins/write-file.js +1 -1
  44. package/dist/tools/registry.js +6 -1
  45. package/dist/ui/layout.js +148 -50
  46. package/dist/ui/prompt.js +4 -4
  47. package/dist/ui/render.js +20 -0
  48. package/package.json +1 -1
@@ -23,6 +23,47 @@ export function truncateMid(text, max) {
23
23
  const out = text.slice(0, head) + marker + text.slice(text.length - tail);
24
24
  return out.length > max ? out.slice(0, max) : out;
25
25
  }
26
+ /**
27
+ * Provenance 压缩:旧 tool_calls.arguments 超长时,把大字符串字段替换为 "<N 字符,已省略>" stub,
28
+ * 保留 path(/rollback planRollback 靠 JSON.parse(arguments).path 找文件,见 rollback/index.ts)+ 其余短字段。
29
+ * 比 truncateMid 的 head+tail 片段更短且更易读:LLM/摘要器看到 "<5000 字符,已省略>" 即知"写过 5000 字符",
30
+ * 而非混乱的 head+tail 片段。
31
+ *
32
+ * 保:JSON 合法(parse/stringify 失败均原样返回)+ tool_call_id 配对不动(只改 arguments 内容,不改
33
+ * tool_calls 数组结构)+ path 永不省(rollback 依赖)。永不抛错(对齐「调度器永不抛错」契约)。
34
+ *
35
+ * 触发:整体 arguments > MAX_OLD_TOOL_STUB 才进(同原 truncateMid 门槛);字段级也 > MAX_OLD_TOOL_STUB 才 stub。
36
+ */
37
+ function stubToolCallArguments(argsRaw) {
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(argsRaw);
41
+ }
42
+ catch {
43
+ return argsRaw; // 非合法 JSON(模型偶发):不碰
44
+ }
45
+ if (!parsed || typeof parsed !== 'object')
46
+ return argsRaw;
47
+ const obj = parsed;
48
+ let changed = false;
49
+ for (const k of Object.keys(obj)) {
50
+ if (k === 'path')
51
+ continue; // path 永不省:/rollback planRollback 靠它定位文件
52
+ const v = obj[k];
53
+ if (typeof v === 'string' && v.length > MAX_OLD_TOOL_STUB) {
54
+ obj[k] = `<${v.length} 字符,已省略>`;
55
+ changed = true;
56
+ }
57
+ }
58
+ if (!changed)
59
+ return argsRaw;
60
+ try {
61
+ return JSON.stringify(obj);
62
+ }
63
+ catch {
64
+ return argsRaw; // stringify 失败(理论不会):不碰
65
+ }
66
+ }
26
67
  /**
27
68
  * push-time 第一层:工具结果进 history 前裁到 MAX_HISTORY_RESULT。
28
69
  * 显示层(summarizeToolResult)仍用原 output,不受影响。
@@ -208,8 +249,8 @@ export async function compactHistory(history, opts) {
208
249
  // 第一层:微压缩——旧区原地截短(保 tool_call_id,无 LLM 调用)
209
250
  // 覆盖三类大字段,均只裁模型/工具产物,不动 user 原话与 system(摘要):
210
251
  // ① tool 结果 content;
211
- // ② 旧 assistant 的 tool_calls.arguments —— 保 JSON 合法:整体超长才进,
212
- // 逐字段值裁中截后重新 stringify(不裸中截会劈断 JSON、严格后端 400);parse 失败跳过;
252
+ // ② 旧 assistant 的 tool_calls.arguments —— provenance stub:整体超长才进,大字段
253
+ // 替换为 "<N 字符,已省略>"(保 path + JSON 合法,见 stubToolCallArguments);
213
254
  // ③ 旧 assistant 正文 content(模型长解释,回看价值低)。
214
255
  let microcompactDone = false;
215
256
  for (const g of oldGroups) {
@@ -227,27 +268,17 @@ export async function compactHistory(history, opts) {
227
268
  as.content = truncateMid(as.content, MAX_OLD_TOOL_STUB);
228
269
  microcompactDone = true;
229
270
  }
230
- // ② tool_calls 参数:整体超长才进,逐字段裁值,保 JSON 合法
271
+ // ② tool_calls 参数:整体超长才进,provenance stub 大字段(保 path + JSON 合法)。
272
+ // stubToolCallArguments:大字符串字段 → "<N 字符,已省略>";path 永不省(/rollback 依赖)。
231
273
  if (Array.isArray(as.tool_calls)) {
232
274
  for (const tc of as.tool_calls) {
233
275
  const args = tc?.function?.arguments;
234
276
  if (typeof args !== 'string' || args.length <= MAX_OLD_TOOL_STUB)
235
277
  continue;
236
- try {
237
- const parsed = JSON.parse(args);
238
- if (parsed && typeof parsed === 'object') {
239
- for (const k of Object.keys(parsed)) {
240
- const v = parsed[k];
241
- if (typeof v === 'string' && v.length > MAX_OLD_TOOL_STUB) {
242
- parsed[k] = truncateMid(v, MAX_OLD_TOOL_STUB);
243
- }
244
- }
245
- tc.function.arguments = JSON.stringify(parsed);
246
- microcompactDone = true;
247
- }
248
- }
249
- catch {
250
- // arguments 非合法 JSON(模型偶发):不裁,沿用「调度器永不抛错」
278
+ const stubbed = stubToolCallArguments(args);
279
+ if (stubbed !== args) {
280
+ tc.function.arguments = stubbed;
281
+ microcompactDone = true;
251
282
  }
252
283
  }
253
284
  }
@@ -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 auto — present 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)}`;