mocode-ai 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +24 -9
  2. package/README.zh-CN.md +355 -341
  3. package/dist/agent/model-turn.js +7 -8
  4. package/dist/agent/run-coordinator.js +4 -0
  5. package/dist/agent/stages/tool-dispatcher.js +1 -0
  6. package/dist/agent/tool-turn.js +1 -1
  7. package/dist/attachments/image.js +119 -2
  8. package/dist/config/index.js +84 -14
  9. package/dist/config/profiles.js +42 -10
  10. package/dist/context/encoders/search.js +6 -2
  11. package/dist/context/vision-window.js +2 -2
  12. package/dist/i18n/index.js +30 -14
  13. package/dist/repl/commands/image.js +7 -2
  14. package/dist/repl/commands/router.js +13 -1
  15. package/dist/repl/commands/system.js +29 -9
  16. package/dist/repl/commands/tool-group.js +1 -1
  17. package/dist/repl/commands.js +2 -0
  18. package/dist/runtime/dev-server-manager.js +4 -2
  19. package/dist/runtime/shell.js +153 -0
  20. package/dist/skills/builtin-skills.js +1 -1
  21. package/dist/tools/builtins/ask-human.js +2 -3
  22. package/dist/tools/builtins/dev-server.js +23 -6
  23. package/dist/tools/builtins/edit-file.js +5 -13
  24. package/dist/tools/builtins/glob.js +2 -2
  25. package/dist/tools/builtins/grep.js +93 -26
  26. package/dist/tools/builtins/index.js +6 -6
  27. package/dist/tools/builtins/note-append.js +4 -8
  28. package/dist/tools/builtins/plan-update.js +1 -6
  29. package/dist/tools/builtins/read-file.js +135 -18
  30. package/dist/tools/builtins/run-command.js +60 -11
  31. package/dist/tools/builtins/screenshot.js +17 -41
  32. package/dist/tools/builtins/use-skill.js +2 -2
  33. package/dist/tools/builtins/web-fetch.js +148 -35
  34. package/dist/tools/builtins/web-search.js +1 -2
  35. package/dist/tools/builtins/write-file.js +102 -7
  36. package/dist/tools/constants.js +7 -0
  37. package/dist/tools/policy.js +26 -7
  38. package/dist/tools/router.js +20 -4
  39. package/dist/tools/tool-runtime.js +63 -1
  40. package/dist/ui/render.js +20 -2
  41. package/package.json +1 -1
@@ -57,6 +57,38 @@ function terminalOutcome(status, code, output, startedAt, changedFiles = []) {
57
57
  durationMs: Date.now() - startedAt,
58
58
  };
59
59
  }
60
+ /**
61
+ * 幂等工具的瞬时失败自动重试(retryable 契约的唯一消费点)。
62
+ *
63
+ * retryable 字段此前全项目零消费者:工具诚实地标了「这是瞬时失败」,却没人据此行动,
64
+ * 模型只能靠再发一轮 tool call 自救(浪费一个完整 LLM 往返 + 常忘记重试)。
65
+ * 现在 runtime 在**工具显式声明 idempotent** 时自动重发:
66
+ * - 只对 capabilities.idempotent=true 的工具(web_fetch/web_search 这类无副作用 GET)生效;
67
+ * 写/进程类工具永不自动重试 —— 重试语义由工具自己决定(如 edit_file 的 expected_hash 冲突)。
68
+ * - 只重试 status='error' 且 retryable=true 的结果:denied/aborted 是终态,重试无意义。
69
+ * - 退避 400ms/1200ms(最多 2 次):429 的 Retry-After 场景通常秒级窗口就够,
70
+ * 再长会把「工具调用」拖成用户可感知的卡顿;仍失败则把重试次数写进 output,模型可自行决策。
71
+ * - abort 敏感:等待期间用户 Ctrl+C 立即放弃,不空耗退避窗口。
72
+ */
73
+ const RETRY_BACKOFF_MS = [400, 1200];
74
+ function sleepWithSignal(ms, signal) {
75
+ return new Promise((resolve) => {
76
+ if (signal?.aborted) {
77
+ resolve();
78
+ return;
79
+ }
80
+ // onAbort 先声明再被 timer 闭包引用:避免 TDZ 依赖「回调必然异步」这一前提。
81
+ const onAbort = () => {
82
+ clearTimeout(timer);
83
+ resolve();
84
+ };
85
+ const timer = setTimeout(() => {
86
+ signal?.removeEventListener('abort', onAbort);
87
+ resolve();
88
+ }, ms);
89
+ signal?.addEventListener('abort', onAbort, { once: true });
90
+ });
91
+ }
60
92
  function isTransientExecutionError(error) {
61
93
  if (!error || typeof error !== 'object')
62
94
  return false;
@@ -179,12 +211,42 @@ export class ToolRuntime {
179
211
  if (sandboxError) {
180
212
  return terminalOutcome('denied', 'SANDBOX_DENIED', sandboxError, startedAt);
181
213
  }
182
- return await this.executeToolOnce(tool, args, signal, opts);
214
+ return await this.executeToolWithRetry(tool, args, signal, opts);
183
215
  }
184
216
  catch (error) {
185
217
  return executionErrorOutcome(name, error, startedAt, []);
186
218
  }
187
219
  }
220
+ /**
221
+ * 幂等工具的自动重试外壳:非幂等工具直接执行一次(与旧行为逐字节一致,零开销)。
222
+ * 只有 capabilities.idempotent=true 且结果 status='error' + retryable=true 才重发。
223
+ */
224
+ async executeToolWithRetry(tool, args, signal, opts) {
225
+ const capabilities = this.getToolCapabilities(tool);
226
+ if (!capabilities.idempotent)
227
+ return this.executeToolOnce(tool, args, signal, opts);
228
+ let outcome = await this.executeToolOnce(tool, args, signal, opts);
229
+ for (const backoff of RETRY_BACKOFF_MS) {
230
+ // 只对瞬时 error 重试;denied/aborted/success 都是终态。abort 后立即停,不空耗退避。
231
+ if (outcome.status !== 'error' || !outcome.retryable || signal?.aborted)
232
+ break;
233
+ // TIMEOUT 不自动重试:一次超时已经烧掉整个超时窗口(web_fetch 是 30s),再试两次最坏
234
+ // 让单次工具调用变成 90s —— 用户看到的是 spinner 长时间冻住。超时通常是目标主机真的慢,
235
+ // 重试收益低、体感代价高。retryable 标记仍保留,模型可自行判断是否再发一轮。
236
+ if (outcome.code === 'TIMEOUT')
237
+ break;
238
+ await sleepWithSignal(backoff, signal);
239
+ if (signal?.aborted)
240
+ break;
241
+ outcome = await this.executeToolOnce(tool, args, signal, opts);
242
+ }
243
+ // 重试用尽仍失败:把尝试次数写进 output,让模型知道「已经自动重试过了,别再无脑重发」。
244
+ // TIMEOUT 走的是「未自动重试」路径,不能谎称重试过。
245
+ if (outcome.status === 'error' && outcome.retryable && outcome.code !== 'TIMEOUT') {
246
+ outcome = { ...outcome, output: `${outcome.output}\n(runtime 已自动重试 ${RETRY_BACKOFF_MS.length} 次仍失败)` };
247
+ }
248
+ return outcome;
249
+ }
188
250
  /** 字符串兼容入口:现有调用方、TUI 和 LLM history 无需同步迁移。 */
189
251
  async executeTool(name, argsRaw, signal, opts) {
190
252
  return (await this.executeToolOutcome(name, argsRaw, signal, opts)).output;
package/dist/ui/render.js CHANGED
@@ -2,6 +2,7 @@ import { stdout } from 'node:process';
2
2
  import { createRequire } from 'node:module';
3
3
  import { ui } from './theme.js';
4
4
  import { t } from '../i18n/index.js';
5
+ import { IMAGE_READ_MARKER } from '../tools/constants.js';
5
6
  let VERSION = '0.0.0';
6
7
  try {
7
8
  VERSION = createRequire(import.meta.url)('../../package.json').version;
@@ -431,16 +432,33 @@ export function summarizeToolCall(name, argsRaw) {
431
432
  return truncateDisplay(argsRaw, 80);
432
433
  }
433
434
  }
435
+ /** grep 每文件头的命中数:`foo.ts: 3 处匹配,行号 [...]`(与 builtins/grep.ts 的输出契约同源)。 */
436
+ const GREP_HEADER_MATCH_COUNT = /:\s*(\d+)\s*处匹配/g;
434
437
  /** 工具结果的人可读一行预览(行数 / 匹配数 / 首行);喂回 LLM 的全文不变。 */
435
438
  export function summarizeToolResult(name, output) {
436
439
  const nonEmpty = output.split('\n').filter((l) => l.trim().length > 0);
437
440
  switch (name) {
438
441
  case 'read_file':
442
+ // 图片分支(read-file.ts IMAGE_READ_MARKER)不是「N 行」文本,单独说清楚,
443
+ // 否则用户看到「1 行」会以为读取失败或被截断。
444
+ if (output.startsWith(IMAGE_READ_MARKER))
445
+ return t('toolSummary.imageAttached');
439
446
  return nonEmpty.length ? t('toolSummary.lines', { count: nonEmpty.length }) : t('toolSummary.emptyFile');
440
447
  case 'glob':
441
448
  return nonEmpty.length ? t('toolSummary.files', { count: nonEmpty.length }) : t('toolSummary.noMatches');
442
- case 'grep':
443
- return nonEmpty.length ? t('toolSummary.matches', { count: nonEmpty.length }) : t('toolSummary.noMatches');
449
+ case 'grep': {
450
+ // 「无匹配」结果不含任何文件头:必须显式识别,否则会被当成 1 处匹配(nonEmpty 只有 1 行)。
451
+ if (!nonEmpty.length || nonEmpty[0].startsWith('无匹配'))
452
+ return t('toolSummary.noMatches');
453
+ // 带 context 时 body 行数 ≠ 命中数(邻居行 + ` --` 分隔都在里面),
454
+ // 按每文件头的真实命中数求和才对得上模型看到的「N 处匹配」。
455
+ let matches = 0;
456
+ GREP_HEADER_MATCH_COUNT.lastIndex = 0;
457
+ for (let m = GREP_HEADER_MATCH_COUNT.exec(output); m; m = GREP_HEADER_MATCH_COUNT.exec(output)) {
458
+ matches += Number(m[1]);
459
+ }
460
+ return matches > 0 ? t('toolSummary.matches', { count: matches }) : t('toolSummary.noMatches');
461
+ }
444
462
  case 'run_command':
445
463
  return truncateDisplay(nonEmpty[0] ?? '', 100) || t('toolSummary.noOutput');
446
464
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 25 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {