mocode-ai 0.1.2 → 0.1.4
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 +34 -7
- package/dist/agent/index.js +134 -60
- package/dist/config/index.js +11 -0
- package/dist/memory/discover.js +49 -0
- package/dist/memory/index.js +45 -0
- package/dist/memory/reflect.js +265 -0
- package/dist/memory/store.js +339 -0
- package/dist/repl/index.js +133 -24
- package/dist/session/compact.js +50 -2
- package/dist/tools/builtins/ask-human.js +50 -0
- package/dist/tools/builtins/index.js +12 -0
- package/dist/tools/builtins/memory-forget.js +32 -0
- package/dist/tools/builtins/memory-list.js +34 -0
- package/dist/tools/builtins/memory-save.js +51 -0
- package/dist/tools/builtins/memory-search.js +42 -0
- package/dist/tools/builtins/memory-update.js +41 -0
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/web-fetch.js +1 -1
- package/dist/tools/builtins/web-search.js +1 -1
- package/dist/tools/constants.js +13 -0
- package/dist/ui/intervention.js +297 -0
- package/dist/ui/layout.js +115 -30
- package/dist/ui/prompt.js +335 -18
- package/dist/ui/render.js +18 -0
- package/dist/ui/spinner.js +15 -0
- package/dist/ui/theme.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,21 +20,43 @@
|
|
|
20
20
|
|
|
21
21
|
## 安装
|
|
22
22
|
|
|
23
|
+
要求 Node.js ≥ 18。
|
|
24
|
+
|
|
23
25
|
```bash
|
|
26
|
+
npm install -g mocode-ai
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
装完即得 `mocode` 命令。不想全局装也可免装直跑:`npx mocode-ai`。
|
|
30
|
+
|
|
31
|
+
> mocode 启动时自动检测新版本,后台 `npm i -g mocode-ai@latest` 自更新——下次启动生效,零启动延迟、断网 / 失败静默。开发态 `npm start`(tsx 跑 `.ts`)不触发。
|
|
32
|
+
|
|
33
|
+
### 从源码运行(开发 / 贡献)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/wanxunyang/mocode.git
|
|
24
37
|
cd mocode
|
|
25
38
|
npm install
|
|
39
|
+
npm start
|
|
26
40
|
```
|
|
27
41
|
|
|
28
|
-
|
|
42
|
+
源码经 tsx 直接跑,无构建步骤。改完代码需重启 `npm start` 生效(tsx 启动时加载模块,不热更新)。依赖:`openai`、`dotenv`、`fast-glob`(运行时);`tsx`、`typescript`、`@types/node`(开发)。
|
|
29
43
|
|
|
30
44
|
## 配置
|
|
31
45
|
|
|
32
|
-
|
|
46
|
+
首次使用运行配置向导,交互填三项(API 地址 / key / 模型名),写入 `~/.mocode/config`(全局,任意目录、任意终端生效):
|
|
33
47
|
|
|
34
48
|
```bash
|
|
35
|
-
|
|
49
|
+
mocode config
|
|
36
50
|
```
|
|
37
51
|
|
|
52
|
+
也可手写配置文件。mocode 按以下优先级加载(后者覆盖前者,仅回填未设置的环境变量;shell 里 `export` 的永远最优先):
|
|
53
|
+
|
|
54
|
+
1. `~/.mocode/config` — 全局(`mocode config` 写此文件)
|
|
55
|
+
2. `<cwd>/.mocode/config` — 项目级覆盖
|
|
56
|
+
3. `<cwd>/.env` — 旧用法兼容(源码仓库内有 `.env.example` 可参考)
|
|
57
|
+
|
|
58
|
+
必填三项:
|
|
59
|
+
|
|
38
60
|
```env
|
|
39
61
|
LLM_BASE_URL=https://open.bigmodel.cn/api/v3 # 换成你的后端
|
|
40
62
|
LLM_API_KEY=your-key-here
|
|
@@ -62,6 +84,8 @@ LLM_MODEL=glm-4.6 # 换成你的模型名
|
|
|
62
84
|
| `COMPACT_THRESHOLD` | 自动压缩触发阈值(占窗口比例) | `0.85` |
|
|
63
85
|
| `LLM_STREAM_USAGE` | 流式请求带 `stream_options.include_usage` 拿真实用量 | `true` |
|
|
64
86
|
| `AUTO_COMPACT` | 自动压缩总开关 | `true` |
|
|
87
|
+
| `AUTO_REFLECT` | 后台反思 pass 总开关(定期从会话挖掘记忆) | `true` |
|
|
88
|
+
| `REFLECT_EVERY_N` | 每 N 轮触发一次后台反思(与 agent 并发,不阻塞) | `5` |
|
|
65
89
|
| `ANYSEARCH_API_KEY` | 联网搜索 API key(不配走匿名免费额度) | 无 |
|
|
66
90
|
| `ANYSEARCH_BASE_URL` | 搜索 API 端点 | `https://api.anysearch.com` |
|
|
67
91
|
| `SKILLS_DIRS` | 覆盖默认 skill 扫描目录(平台分隔符) | 三目录自动扫描 |
|
|
@@ -69,14 +93,17 @@ LLM_MODEL=glm-4.6 # 换成你的模型名
|
|
|
69
93
|
## 运行
|
|
70
94
|
|
|
71
95
|
```bash
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
96
|
+
mocode # 新会话(在目标项目目录里跑)
|
|
97
|
+
mocode --resume # 列出已保存会话
|
|
98
|
+
mocode --resume <id> # 续接指定会话
|
|
99
|
+
mocode config # 改配置
|
|
75
100
|
```
|
|
76
101
|
|
|
102
|
+
从源码跑则用 `npm start`(等价于 `mocode`,但不触发自更新)。
|
|
103
|
+
|
|
77
104
|
进入 REPL 后直接对话。启动即进全屏 TUI,显示横幅(模型 / 后端 / 工作目录 / 工具列表)。回复流式打印,思考段实时可见后折叠。
|
|
78
105
|
|
|
79
|
-
agent 工作在**启动时所在的工作目录**——想让它操作某个项目,就 `cd` 到那个项目再 `
|
|
106
|
+
agent 工作在**启动时所在的工作目录**——想让它操作某个项目,就 `cd` 到那个项目再 `mocode`。
|
|
80
107
|
|
|
81
108
|
## 工具
|
|
82
109
|
|
package/dist/agent/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { chat, } from '../llm/index.js';
|
|
|
4
4
|
import { executeTool } from '../tools/registry.js';
|
|
5
5
|
import { ui } from '../ui/theme.js';
|
|
6
6
|
import { Spinner } from '../ui/spinner.js';
|
|
7
|
-
import { summarizeToolCall, summarizeToolResult, truncateDisplay, } from '../ui/render.js';
|
|
7
|
+
import { summarizeToolCall, summarizeToolResult, truncateDisplay, fmtElapsed, } from '../ui/render.js';
|
|
8
8
|
import { renderFileChange } from '../ui/diff.js';
|
|
9
9
|
import * as layout from '../ui/layout.js';
|
|
10
10
|
import { beginTurn } from '../rollback/index.js';
|
|
@@ -19,6 +19,84 @@ function parseArgs(raw) {
|
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/** 只读工具集:一轮多个时,连续的只读工具成组 Promise.all 并行(无副作用、互不依赖)。 */
|
|
23
|
+
const READ_TOOL_NAMES = new Set([
|
|
24
|
+
'read_file',
|
|
25
|
+
'glob',
|
|
26
|
+
'grep',
|
|
27
|
+
'web_search',
|
|
28
|
+
'web_fetch',
|
|
29
|
+
]);
|
|
30
|
+
/** mutation 工具:写盘 + 在 executeTool 内记回滚 before 快照,必须串行保快照序。 */
|
|
31
|
+
const isMutationTool = (name) => name === 'edit_file' || name === 'write_file';
|
|
32
|
+
/** 工具调用 ● 头:工具名 + 参数摘要(按 tool_calls 原顺序打印,让用户看到本轮跑哪些工具)。 */
|
|
33
|
+
function writeToolHeader(tc) {
|
|
34
|
+
const summary = summarizeToolCall(tc.name, tc.arguments);
|
|
35
|
+
layout.contentWrite(` ${ui.brightMagenta}●${ui.reset} ${ui.cyan}${tc.name}${ui.reset} ${ui.dim}${summary}${ui.reset}\n`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* mutation 执行前读旧内容供 diff:write_file 取整文件旧内容(不存在→null=新建),
|
|
39
|
+
* edit_file 取 old_string 起始行号(供 diff 显示真实文件行号)。读不到则 diff 退化为相对行号。
|
|
40
|
+
* 非 mutation 或参数非法返 { preWriteOld: null, editStartLine: 1 }。失败不阻断。
|
|
41
|
+
*/
|
|
42
|
+
function readDiffContext(tc, parsed) {
|
|
43
|
+
if (!parsed)
|
|
44
|
+
return { preWriteOld: null, editStartLine: 1 };
|
|
45
|
+
const p = String(parsed.path ?? '');
|
|
46
|
+
if (!p)
|
|
47
|
+
return { preWriteOld: null, editStartLine: 1 };
|
|
48
|
+
if (tc.name === 'write_file') {
|
|
49
|
+
try {
|
|
50
|
+
return { preWriteOld: readFileSync(resolve(p), 'utf8'), editStartLine: 1 };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { preWriteOld: null, editStartLine: 1 }; // 文件不存在(新建)或不可读
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (tc.name === 'edit_file') {
|
|
57
|
+
const oldStr = String(parsed.old_string ?? '');
|
|
58
|
+
try {
|
|
59
|
+
const data = readFileSync(resolve(p), 'utf8');
|
|
60
|
+
const idx = oldStr ? data.indexOf(oldStr) : -1;
|
|
61
|
+
return {
|
|
62
|
+
preWriteOld: null,
|
|
63
|
+
editStartLine: idx >= 0 ? data.slice(0, idx).split('\n').length : 1,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { preWriteOld: null, editStartLine: 1 }; // 读不到:diff 退化为相对行号
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { preWriteOld: null, editStartLine: 1 };
|
|
71
|
+
}
|
|
72
|
+
/** 渲染工具结果:mutation 成功走 diff 块(行号 + 语法高亮,仿 Claude Code);其余走一行 preview。 */
|
|
73
|
+
function writeToolResult(tc, output, parsed, preWriteOld, editStartLine) {
|
|
74
|
+
if (isMutationTool(tc.name) && parsed && !output.startsWith('错误')) {
|
|
75
|
+
layout.contentWrite(renderFileChange({
|
|
76
|
+
path: String(parsed.path ?? ''),
|
|
77
|
+
kind: tc.name === 'edit_file' ? 'edit' : 'write',
|
|
78
|
+
oldStr: tc.name === 'edit_file'
|
|
79
|
+
? String(parsed.old_string ?? '')
|
|
80
|
+
: preWriteOld,
|
|
81
|
+
newStr: String((tc.name === 'edit_file' ? parsed.new_string : parsed.content) ?? ''),
|
|
82
|
+
startLine: tc.name === 'edit_file' ? editStartLine : 1,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const preview = summarizeToolResult(tc.name, output);
|
|
87
|
+
if (preview) {
|
|
88
|
+
layout.contentWrite(` ${ui.gray}↳ ${preview}${ui.reset}\n`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** 回灌 tool 结果到 history(裁到单条上限);tool_call_id 与 assistant.tool_calls 按序配对。 */
|
|
93
|
+
function pushToolResult(history, tc, output) {
|
|
94
|
+
history.push({
|
|
95
|
+
role: 'tool',
|
|
96
|
+
tool_call_id: tc.id,
|
|
97
|
+
content: capToolResultForHistory(tc.name, output),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
22
100
|
/**
|
|
23
101
|
* agent 核心循环:
|
|
24
102
|
* 流式调 LLM(onText / onThinking 实时写内容区)→ 有 tool_calls 就执行并回灌
|
|
@@ -31,10 +109,16 @@ function parseArgs(raw) {
|
|
|
31
109
|
* 思考段用 layout.beginSegment / eraseSegmentBack 精确按物理行折叠(修预存按 \n 数行漏擦 bug),
|
|
32
110
|
* 原文存入 collapsedThinkings 供 /think N 重打。
|
|
33
111
|
*/
|
|
34
|
-
export async function runAgent(history, userInput, collapsedThinkings = [], signal
|
|
112
|
+
export async function runAgent(history, userInput, collapsedThinkings = [], signal,
|
|
113
|
+
/** 每步 chat() 返回后回调:repl 据此重算并重画状态行 context 用量条(运行中实时刷新,不冻结在轮首)。 */
|
|
114
|
+
onContextUpdate) {
|
|
35
115
|
// 中断回滚快照:入口(本 turn push 任何消息前)整段浅拷贝。abort 时 length=0;push(...saved) 还原。
|
|
36
116
|
// 用 slice() 而非 length:maybeCompact 会原地重建(length=0;push(...rebuilt)),savedLen 会失效。
|
|
37
117
|
const savedHistory = history.slice();
|
|
118
|
+
// 本轮计时:从入口到完毕(正常 return / 达上限),供 finally 打 ✻ Worked for 摘要行。
|
|
119
|
+
// 与 layout 的 turnStart 各自独立(差几毫秒),避免 agent 反向读 layout 状态的耦合。
|
|
120
|
+
const t0 = Date.now();
|
|
121
|
+
let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
|
|
38
122
|
history.push({ role: 'user', content: userInput });
|
|
39
123
|
// 开新轮次(回滚用):首行截断 40,供 /rollback 轮次菜单展示。
|
|
40
124
|
beginTurn(truncateDisplay(userInput.split('\n')[0] ?? '', 40));
|
|
@@ -138,6 +222,8 @@ export async function runAgent(history, userInput, collapsedThinkings = [], sign
|
|
|
138
222
|
}
|
|
139
223
|
contextState.lastUsage = result.usage; // 供 /context 与状态行显示实测 token
|
|
140
224
|
spinner.stop();
|
|
225
|
+
// lastUsage 已更新:触发状态行 context 用量条重算+重画,运行中不再冻结在轮首。
|
|
226
|
+
onContextUpdate?.();
|
|
141
227
|
if (result.toolCalls.length > 0) {
|
|
142
228
|
// 思考后直接进入 tool_call(无 text):先把可见的思考段折叠
|
|
143
229
|
flushThinkCollapsed();
|
|
@@ -154,68 +240,50 @@ export async function runAgent(history, userInput, collapsedThinkings = [], sign
|
|
|
154
240
|
function: { name: tc.name, arguments: tc.arguments },
|
|
155
241
|
})),
|
|
156
242
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (idx >= 0)
|
|
184
|
-
editStartLine = data.slice(0, idx).split('\n').length;
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
// 读不到:startLine 保持 1(diff 退化为相对行号)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
243
|
+
// 工具分组执行(保 tool_calls 原顺序):连续的只读工具(READ_TOOL_NAMES)成组并发——一次性启动全部
|
|
244
|
+
// executeTool(调用即开始 I/O),再按原顺序逐个 await + 渲染(● 头与 ↳ 结果紧邻,修并行时"全 ● 后全 ↳"分离)。
|
|
245
|
+
// mutation(write_file/edit_file)及 run_command/use_skill 各为单步串行屏障——mutation 串行保
|
|
246
|
+
// recordMutation 调用序 = 回滚快照序(executeTool 内写前记 before 快照,同文件多次写需按序)。
|
|
247
|
+
// 渲染与 history 回灌一律按原顺序;并发只影响执行时序,tool_call_id 仍按序配对。
|
|
248
|
+
// executeTool 永不抛错(调度器 try/catch 返字符串),故 await 单个 promise 不会抛(永远 resolve 为字符串)。
|
|
249
|
+
const calls = result.toolCalls;
|
|
250
|
+
let i = 0;
|
|
251
|
+
while (i < calls.length) {
|
|
252
|
+
if (READ_TOOL_NAMES.has(calls[i].name)) {
|
|
253
|
+
// 收集连续只读组(≥1),并发执行:先一次性启动所有(executeTool 调用即开始 I/O),
|
|
254
|
+
// 再按原顺序逐个 await + 渲染——● 头与 ↳ 结果紧邻、顺序 = tool_calls 序(修"全 ● 后全 ↳"分离 bug)。
|
|
255
|
+
// 异步工具(web_fetch 等)并发跑、总耗时 ≈ 最慢一个;同步工具(glob/grep)map 时已顺序跑完,await 即返。
|
|
256
|
+
let j = i;
|
|
257
|
+
while (j < calls.length && READ_TOOL_NAMES.has(calls[j].name))
|
|
258
|
+
j++;
|
|
259
|
+
const batch = calls.slice(i, j);
|
|
260
|
+
const started = batch.map((tc) => executeTool(tc.name, tc.arguments));
|
|
261
|
+
for (let k = 0; k < batch.length; k++) {
|
|
262
|
+
const tc = batch[k];
|
|
263
|
+
writeToolHeader(tc);
|
|
264
|
+
spinner.start(`执行 ${tc.name}`);
|
|
265
|
+
const output = await started[k];
|
|
266
|
+
spinner.stop();
|
|
267
|
+
writeToolResult(tc, output, null, null, 1); // 只读工具无 diff
|
|
268
|
+
pushToolResult(history, tc, output);
|
|
190
269
|
}
|
|
191
|
-
|
|
192
|
-
spinner.start(`执行 ${tc.name}`);
|
|
193
|
-
const output = await executeTool(tc.name, tc.arguments);
|
|
194
|
-
spinner.stop();
|
|
195
|
-
// edit_file / write_file 成功:渲染 diff 块(行号 + 语法高亮,仿 Claude Code);其余工具走一行 preview。
|
|
196
|
-
if (isMutation && parsed && !output.startsWith('错误')) {
|
|
197
|
-
layout.contentWrite(renderFileChange({
|
|
198
|
-
path: String(parsed.path ?? ''),
|
|
199
|
-
kind: tc.name === 'edit_file' ? 'edit' : 'write',
|
|
200
|
-
oldStr: tc.name === 'edit_file'
|
|
201
|
-
? String(parsed.old_string ?? '')
|
|
202
|
-
: preWriteOld,
|
|
203
|
-
newStr: String((tc.name === 'edit_file' ? parsed.new_string : parsed.content) ??
|
|
204
|
-
''),
|
|
205
|
-
startLine: tc.name === 'edit_file' ? editStartLine : 1,
|
|
206
|
-
}));
|
|
270
|
+
i = j;
|
|
207
271
|
}
|
|
208
272
|
else {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
273
|
+
// 单步串行(mutation / run_command / use_skill)——逐个执行,保快照序
|
|
274
|
+
const tc = calls[i];
|
|
275
|
+
writeToolHeader(tc);
|
|
276
|
+
const parsed = isMutationTool(tc.name)
|
|
277
|
+
? parseArgs(tc.arguments)
|
|
278
|
+
: null;
|
|
279
|
+
const { preWriteOld, editStartLine } = readDiffContext(tc, parsed);
|
|
280
|
+
spinner.start(`执行 ${tc.name}`);
|
|
281
|
+
const output = await executeTool(tc.name, tc.arguments);
|
|
282
|
+
spinner.stop();
|
|
283
|
+
writeToolResult(tc, output, parsed, preWriteOld, editStartLine);
|
|
284
|
+
pushToolResult(history, tc, output);
|
|
285
|
+
i++;
|
|
213
286
|
}
|
|
214
|
-
history.push({
|
|
215
|
-
role: 'tool',
|
|
216
|
-
tool_call_id: tc.id,
|
|
217
|
-
content: capToolResultForHistory(tc.name, output),
|
|
218
|
-
});
|
|
219
287
|
}
|
|
220
288
|
continue; // 带着工具结果再调一次 LLM
|
|
221
289
|
}
|
|
@@ -227,11 +295,17 @@ export async function runAgent(history, userInput, collapsedThinkings = [], sign
|
|
|
227
295
|
if (!gotText)
|
|
228
296
|
layout.contentWrite(`${ui.dim}(无回复)${ui.reset}\n`);
|
|
229
297
|
history.push({ role: 'assistant', content: result.content });
|
|
298
|
+
done = true;
|
|
230
299
|
return;
|
|
231
300
|
}
|
|
232
301
|
layout.contentWrite(` ${ui.yellow}●${ui.reset} ${ui.yellow}达到最大步数(${MAX_STEPS}),本轮停止。${ui.reset}\n`);
|
|
302
|
+
done = true;
|
|
233
303
|
}
|
|
234
304
|
finally {
|
|
235
305
|
spinner.stop();
|
|
306
|
+
// 跑完(正常 / 达上限)在回复末尾打耗时摘要行(仿 Claude Code);中断 done=false 不打。
|
|
307
|
+
if (done) {
|
|
308
|
+
layout.contentWrite(` ${ui.dim}✻ Worked for ${fmtElapsed(Date.now() - t0)}${ui.reset}\n`);
|
|
309
|
+
}
|
|
236
310
|
}
|
|
237
311
|
}
|
package/dist/config/index.js
CHANGED
|
@@ -49,9 +49,11 @@ const SYSTEM_PROMPT = `你是 mocode,一个终端编码 agent。你以"思考
|
|
|
49
49
|
- 改代码前先 read_file 确认实际内容(带行号),不凭记忆猜。
|
|
50
50
|
- 局部改用 edit_file:old_string 须唯一且精确匹配(含缩进/换行),多带上下文行确保唯一;新建或整体重写用 write_file。
|
|
51
51
|
- 找路径用 glob,找内容用 grep;不要用 run_command 拼 cat / sed / find / grep。
|
|
52
|
+
- 若当前目录存在 .codegraph/(已建代码索引),理解/定位代码、查调用链、看改动影响面时必须先 codegraph 再动手:run_command 跑 codegraph explore "<符号或问题>"(一次拿相关符号源码 + 调用路径)或 codegraph node <符号或文件>(单符号源码 + 调用者)。不要逐文件 glob/read_file/grep 去拼凑理解——那是 codegraph 已替你做完的事。仅当 codegraph 找不到、未索引、要看刚改的最新内容、或改单个已知小文件时才用 read_file/grep/glob。详见 use_skill codegraph。
|
|
52
53
|
- run_command 按平台执行(Win 用 cmd、其他用 bash);有副作用的命令(删文件、装包、git push、重置等)执行前先简述意图。
|
|
53
54
|
- 需要训练数据之外的最新信息(新版本、新闻、实时数据、最新 API)时用 web_search 联网搜索,不要凭记忆答可能过时的内容。
|
|
54
55
|
- 要读取某个具体 URL 的内容(搜索结果里的链接、用户给的 URL)时用 web_fetch 抓取;它只抓静态 HTML,JS 渲染页面拿不到正文时改用 web_search(其结果自带清洗后的正文)。
|
|
56
|
+
- 遇到需要用户决策的岔路(多种实现方案、不确定用户意图、需要额外信息才能继续),调 ask_human 列出选项让用户选(用户也可选"自定义输入"自由作答)。不要在任务明确、能自行决定时频繁打扰用户;用户取消后换方案或基于已有信息推进,不原地重复问。
|
|
55
57
|
|
|
56
58
|
## 失败处理
|
|
57
59
|
- 工具以字符串返回错误(edit_file 未匹配或不唯一、run_command 非零退出码等)。先分析根因,调整后重试,不要原样重发同一条调用。
|
|
@@ -61,6 +63,13 @@ const SYSTEM_PROMPT = `你是 mocode,一个终端编码 agent。你以"思考
|
|
|
61
63
|
- 不可逆或外向操作(删除、覆盖既有文件、推送、请求外部服务)执行前向用户确认,除非已获明确授权。
|
|
62
64
|
- 只在授权范围内操作;不确定就问,别猜。
|
|
63
65
|
|
|
66
|
+
## 记忆(跨会话长期事实)
|
|
67
|
+
- 系统提示已注入「记忆索引」(仅 id/标题/摘要)。需要某条正文时调 memory_search(传 id 或关键词)取;memory_list 看全部索引。
|
|
68
|
+
- 遇到非显然、跨会话有用的事实/决策/坑(架构约定、易踩坑、用户偏好、已做决策),用 memory_save 存——只存长期稳定项,不存当前 bug / 临时文件 / 未决 TODO。
|
|
69
|
+
- 发现已存记忆过时或与新事实矛盾,用 memory_update(id, …) 原地纠正(别新建重复条);明确失效的用 memory_forget(id) 归档。
|
|
70
|
+
- 存前先 memory_search 看是否已有同类条,避免重复。宁可少记,不记正确废话。
|
|
71
|
+
- 后台反思 pass 会定期从会话里挖掘并整理记忆(无需你手动),但你主动存的关键事实更可靠。
|
|
72
|
+
|
|
64
73
|
## 终止与汇报
|
|
65
74
|
- 无需更多工具时立即停止,直接给结论。
|
|
66
75
|
- 如实汇报:成功说成功,失败说卡在哪,跳过的也要说。引用代码用 "path:行号" 格式(如 src/index.ts:42)。保持简洁。`;
|
|
@@ -74,6 +83,8 @@ export const config = {
|
|
|
74
83
|
compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
|
|
75
84
|
includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
|
|
76
85
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
86
|
+
autoReflect: process.env.AUTO_REFLECT !== 'false',
|
|
87
|
+
reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
|
|
77
88
|
sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
|
|
78
89
|
searchApiKey: process.env.ANYSEARCH_API_KEY,
|
|
79
90
|
searchBaseUrl: process.env.ANYSEARCH_BASE_URL || 'https://api.anysearch.com',
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// memory 发现子系统:加载项目记忆 MOCODE.md(对标 skills/discover.ts 的叶子模式)。
|
|
2
|
+
// 仅依赖 node 标准库,是叶子模块:不依赖 config/agent/llm/tools/skills,避免环。
|
|
3
|
+
//
|
|
4
|
+
// 约定:纯 MOCODE.md(不读 CLAUDE.md——mocode 是独立工具,接非 Claude 后端,
|
|
5
|
+
// 有自己的工具集与约定,叫 CLAUDE.md 名不副实且可能读到 Claude 专属内容)。
|
|
6
|
+
// 项目级从 cwd 向上逐级找,全局 ~/.mocode/MOCODE.md。全量注入 systemPrompt(超长截断);
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
/**
|
|
11
|
+
* 返回要查找的 MOCODE.md 路径列表,按「远→近」顺序(合并时近的在后,更突出):
|
|
12
|
+
* 全局 ~/.mocode/MOCODE.md → 项目级从根到 cwd 逐级 MOCODE.md。
|
|
13
|
+
* 向上遍历 cwd 到根收集 [cwd..root],反转为 [root..cwd](远→近),前拼全局。
|
|
14
|
+
*/
|
|
15
|
+
export function resolveMemoryFiles() {
|
|
16
|
+
const globalPath = path.join(os.homedir(), '.mocode', 'MOCODE.md');
|
|
17
|
+
const projectFiles = [];
|
|
18
|
+
let dir = process.cwd();
|
|
19
|
+
const root = path.parse(dir).root; // win32 'C:\\', POSIX '/'
|
|
20
|
+
for (;;) {
|
|
21
|
+
projectFiles.push(path.join(dir, 'MOCODE.md'));
|
|
22
|
+
if (dir === root || dir === path.dirname(dir))
|
|
23
|
+
break; // 到根:dirname 自身
|
|
24
|
+
dir = path.dirname(dir);
|
|
25
|
+
}
|
|
26
|
+
projectFiles.reverse(); // root..cwd(远→近)
|
|
27
|
+
return [globalPath, ...projectFiles];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 读取所有存在的 MOCODE.md,返回 { path, content }(content 已 trim)。
|
|
31
|
+
* 全程静默容错(不存在 / 读失败 → 跳过,不抛),风格对齐 skills/discover.ts 与 session/persist.ts。
|
|
32
|
+
*/
|
|
33
|
+
export function loadMemoryFiles() {
|
|
34
|
+
const files = resolveMemoryFiles();
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const p of files) {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(p))
|
|
39
|
+
continue;
|
|
40
|
+
const content = readFileSync(p, 'utf8').trim();
|
|
41
|
+
if (content)
|
|
42
|
+
out.push({ path: p, content });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue; // 读失败静默跳过
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// memory barrel:Tier-1 MOCODE.md 懒加载(buildMemorySection,叶子)+ Tier-2 工具库 re-export。
|
|
2
|
+
// Tier-2:store.ts(叶子,node:fs)做 JSONL CRUD/GC/索引段;reflect.ts(→llm,同 session/)做后台反思 pass。
|
|
3
|
+
// 被 repl 依赖(注入 systemPrompt + 轮末触发反思 + 退出 drain)。Tier-1 仅依赖 discover.ts。
|
|
4
|
+
import { loadMemoryFiles } from './discover.js';
|
|
5
|
+
export { buildMemoryIndexSection, loadAll, gcMemories, } from './store.js';
|
|
6
|
+
export { kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, runReflection, } from './reflect.js';
|
|
7
|
+
/** system 消息中 memory 段的字符上限(防过大占窗口——system 在 history[0],compactHistory 不压缩)。 */
|
|
8
|
+
const MAX_MEMORY_CHARS = 20000;
|
|
9
|
+
let cache = null;
|
|
10
|
+
/**
|
|
11
|
+
* 合并全局 + 项目各级 MOCODE.md(远→近拼接,各段空行分隔),超 MAX_MEMORY_CHARS 截断 + 提示。
|
|
12
|
+
* 懒加载(首次调用触发扫描;启动期 repl 调一次)。无 MOCODE.md 返空串。
|
|
13
|
+
*/
|
|
14
|
+
export function loadMemory() {
|
|
15
|
+
if (cache !== null)
|
|
16
|
+
return cache;
|
|
17
|
+
const files = loadMemoryFiles();
|
|
18
|
+
if (files.length === 0) {
|
|
19
|
+
cache = '';
|
|
20
|
+
return cache;
|
|
21
|
+
}
|
|
22
|
+
const body = files.map((f) => f.content).join('\n\n');
|
|
23
|
+
if (body.length <= MAX_MEMORY_CHARS) {
|
|
24
|
+
cache = body;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
cache =
|
|
28
|
+
body.slice(0, MAX_MEMORY_CHARS) +
|
|
29
|
+
`\n\n…(项目记忆已截断 ${body.length - MAX_MEMORY_CHARS} 字符,完整见各级 MOCODE.md)`;
|
|
30
|
+
}
|
|
31
|
+
return cache;
|
|
32
|
+
}
|
|
33
|
+
/** 拼进系统提示的 memory 段;无 memory 返空串(零行为变化)。 */
|
|
34
|
+
export function buildMemorySection() {
|
|
35
|
+
const mem = loadMemory();
|
|
36
|
+
if (!mem)
|
|
37
|
+
return '';
|
|
38
|
+
return [
|
|
39
|
+
'',
|
|
40
|
+
'',
|
|
41
|
+
'## 项目记忆(MOCODE.md)',
|
|
42
|
+
'以下是项目记忆(架构 / 约定 / 命令等跨会话长期事实),据此行动:',
|
|
43
|
+
mem,
|
|
44
|
+
].join('\n');
|
|
45
|
+
}
|