mocode-ai 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -188
- package/dist/agent/core.js +302 -0
- package/dist/agent/index.js +62 -249
- package/dist/agent/mode.js +46 -0
- package/dist/agent/spawn.js +123 -0
- package/dist/commands/config.js +8 -26
- package/dist/config/file.js +48 -0
- package/dist/config/index.js +73 -30
- package/dist/llm/index.js +11 -9
- package/dist/memory/index.js +2 -2
- package/dist/memory/reflect.js +11 -11
- package/dist/memory/store.js +4 -4
- package/dist/repl/index.js +264 -108
- package/dist/session/compact.js +1 -1
- package/dist/skills/index.js +2 -2
- package/dist/tools/builtins/ask-human.js +7 -7
- package/dist/tools/builtins/codegraph.js +113 -0
- package/dist/tools/builtins/edit-file.js +3 -3
- package/dist/tools/builtins/glob.js +3 -2
- package/dist/tools/builtins/grep.js +4 -3
- package/dist/tools/builtins/index.js +7 -1
- package/dist/tools/builtins/memory-forget.js +2 -2
- package/dist/tools/builtins/memory-list.js +2 -2
- package/dist/tools/builtins/memory-save.js +7 -7
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/memory-update.js +6 -6
- package/dist/tools/builtins/read-file.js +5 -4
- package/dist/tools/builtins/run-command.js +40 -7
- package/dist/tools/builtins/switch-mode.js +46 -0
- package/dist/tools/builtins/task.js +62 -0
- package/dist/tools/builtins/use-skill.js +2 -2
- package/dist/tools/builtins/web-fetch.js +16 -3
- package/dist/tools/builtins/web-search.js +6 -6
- package/dist/tools/builtins/write-file.js +3 -3
- package/dist/tools/constants.js +16 -0
- package/dist/tools/registry.js +11 -4
- package/dist/ui/content.js +25 -17
- package/dist/ui/diff.js +28 -26
- package/dist/ui/intervention.js +24 -3
- package/dist/ui/layout.js +295 -81
- package/dist/ui/markdown.js +607 -0
- package/dist/ui/mouse.js +93 -0
- package/dist/ui/prompt.js +259 -5
- package/dist/ui/render.js +1 -1
- package/dist/ui/theme.js +146 -13
- package/package.json +2 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { MAX_OUTPUT } from '../constants.js';
|
|
5
|
+
// ---------- codegraph ----------
|
|
6
|
+
// 已建代码索引(.codegraph/)的仓库里,理解/定位代码、查调用链、看改动影响面时
|
|
7
|
+
// 的首选工具。比逐文件 read_file/grep 拼凑更准更省。
|
|
8
|
+
// 检查 .codegraph/ 是否存在:不存在则提示建索引,不盲目调 CLI(避免报错噪音)。
|
|
9
|
+
/** 同步判断当前 cwd 是否已建 codegraph 索引。 */
|
|
10
|
+
function hasCodegraphIndex() {
|
|
11
|
+
return existsSync(path.join(process.cwd(), '.codegraph'));
|
|
12
|
+
}
|
|
13
|
+
/** 跑 codegraph CLI 子进程,返回合并 stdout+stderr(带退出码),超时 60s。 */
|
|
14
|
+
function runCodegraph(args) {
|
|
15
|
+
return new Promise((done) => {
|
|
16
|
+
const isWin = process.platform === 'win32';
|
|
17
|
+
// win32 必须走 cmd.exe /c:Node 自 CVE-2024-27980 修复后,shell:false 直接 spawn
|
|
18
|
+
// .cmd/.bat 会抛 EINVAL;cmd.exe 是真二进制,直接 spawn 安全(与 run_command 同约定)。
|
|
19
|
+
const child = isWin
|
|
20
|
+
? spawn('cmd.exe', ['/c', 'codegraph', ...args], { cwd: process.cwd() })
|
|
21
|
+
: spawn('codegraph', args, { cwd: process.cwd() });
|
|
22
|
+
let out = '';
|
|
23
|
+
let finished = false;
|
|
24
|
+
const finish = (s) => {
|
|
25
|
+
if (finished)
|
|
26
|
+
return;
|
|
27
|
+
finished = true;
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
done(s);
|
|
30
|
+
};
|
|
31
|
+
const onChunk = (chunk) => {
|
|
32
|
+
if (out.length < MAX_OUTPUT)
|
|
33
|
+
out += chunk.toString('utf8');
|
|
34
|
+
};
|
|
35
|
+
child.stdout.on('data', onChunk);
|
|
36
|
+
child.stderr.on('data', onChunk);
|
|
37
|
+
child.on('error', (e) => finish(`codegraph 执行失败(可能未安装或不在 PATH): ${e.message}\n` +
|
|
38
|
+
'安装: npm i -g @colbymchenry/codegraph'));
|
|
39
|
+
child.on('close', (code) => {
|
|
40
|
+
let r = out.trim();
|
|
41
|
+
if (out.length >= MAX_OUTPUT)
|
|
42
|
+
r += '\n...(输出已截断)';
|
|
43
|
+
finish(`[退出码 ${code}]\n${r || '(无输出)'}`);
|
|
44
|
+
});
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
child.kill();
|
|
47
|
+
finish(`[超时,已终止]\n${out.trim()}`);
|
|
48
|
+
}, 60000);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export const codegraphTool = {
|
|
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`).',
|
|
56
|
+
parameters: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
action: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
enum: ['explore', 'node'],
|
|
62
|
+
description: "explore=fetch relevant source + call paths by question/symbol (use when understanding a feature/architecture);" +
|
|
63
|
+
"node=look up a single symbol's source + callers/callees, or read a file + dependencies (use when locating a specific symbol)",
|
|
64
|
+
},
|
|
65
|
+
query: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'explore: the question or symbol name to investigate (e.g. "runAgent" or "how history is compacted").' +
|
|
68
|
+
'node: symbol name (e.g. runAgent) or file path (used with file mode)',
|
|
69
|
+
},
|
|
70
|
+
file: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'node only: treat query as a file path to read (file mode), or disambiguate to a specific file',
|
|
73
|
+
},
|
|
74
|
+
offset: {
|
|
75
|
+
type: 'integer',
|
|
76
|
+
description: 'node file mode only: start line (1-based)',
|
|
77
|
+
},
|
|
78
|
+
limit: {
|
|
79
|
+
type: 'integer',
|
|
80
|
+
description: 'node file mode only: max number of lines',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
required: ['action', 'query'],
|
|
84
|
+
},
|
|
85
|
+
async execute(args) {
|
|
86
|
+
if (!hasCodegraphIndex()) {
|
|
87
|
+
return ('当前目录无 .codegraph/ 索引。codegraph 工具不可用。\n' +
|
|
88
|
+
'建索引:运行 `codegraph init`(需要 codegraph CLI:`npm i -g @colbymchenry/codegraph`)。\n' +
|
|
89
|
+
'在此之前,可改用 read_file / grep / glob 探索代码。');
|
|
90
|
+
}
|
|
91
|
+
const action = String(args.action);
|
|
92
|
+
const query = String(args.query);
|
|
93
|
+
const cliArgs = [];
|
|
94
|
+
if (action === 'explore') {
|
|
95
|
+
// explore 接 <query...> 多 token,拆分后逐个 push
|
|
96
|
+
cliArgs.push('explore', ...query.split(/\s+/).filter(Boolean));
|
|
97
|
+
}
|
|
98
|
+
else if (action === 'node') {
|
|
99
|
+
cliArgs.push('node', query);
|
|
100
|
+
const file = args.file ? String(args.file) : '';
|
|
101
|
+
if (file)
|
|
102
|
+
cliArgs.push('--file', file);
|
|
103
|
+
if (args.offset !== undefined)
|
|
104
|
+
cliArgs.push('--offset', String(args.offset));
|
|
105
|
+
if (args.limit !== undefined)
|
|
106
|
+
cliArgs.push('--limit', String(args.limit));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
return `错误:未知 action "${action}",可选 explore 或 node。`;
|
|
110
|
+
}
|
|
111
|
+
return runCodegraph(cliArgs);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -3,13 +3,13 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
// ---------- edit_file ----------
|
|
4
4
|
export const editFileTool = {
|
|
5
5
|
name: 'edit_file',
|
|
6
|
-
description: '
|
|
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.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
10
10
|
path: { type: 'string' },
|
|
11
|
-
old_string: { type: 'string', description: '
|
|
12
|
-
new_string: { type: 'string', description: '
|
|
11
|
+
old_string: { type: 'string', description: 'The original text to be replaced; must match exactly' },
|
|
12
|
+
new_string: { type: 'string', description: 'The new text to replace it with' },
|
|
13
13
|
},
|
|
14
14
|
required: ['path', 'old_string', 'new_string'],
|
|
15
15
|
},
|
|
@@ -3,11 +3,12 @@ import { IGNORE } from '../constants.js';
|
|
|
3
3
|
// ---------- glob ----------
|
|
4
4
|
export const globTool = {
|
|
5
5
|
name: 'glob',
|
|
6
|
-
description: '
|
|
6
|
+
description: 'Find file paths matching a glob pattern (e.g. **/*.ts). Returns a list of matches (auto-excludes node_modules / .git).' +
|
|
7
|
+
' 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
8
|
parameters: {
|
|
8
9
|
type: 'object',
|
|
9
10
|
properties: {
|
|
10
|
-
pattern: { type: 'string', description: '
|
|
11
|
+
pattern: { type: 'string', description: 'Glob pattern, e.g. **/*.ts or src/**/*.json' },
|
|
11
12
|
},
|
|
12
13
|
required: ['pattern'],
|
|
13
14
|
},
|
|
@@ -5,12 +5,13 @@ import { MAX_RESULTS, IGNORE } from '../constants.js';
|
|
|
5
5
|
// ---------- grep ----------
|
|
6
6
|
export const grepTool = {
|
|
7
7
|
name: 'grep',
|
|
8
|
-
description: '
|
|
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.',
|
|
9
10
|
parameters: {
|
|
10
11
|
type: 'object',
|
|
11
12
|
properties: {
|
|
12
|
-
pattern: { type: 'string', description: '
|
|
13
|
-
glob: { type: 'string', description: '
|
|
13
|
+
pattern: { type: 'string', description: 'Regular expression' },
|
|
14
|
+
glob: { type: 'string', description: 'Optional, restrict to a file glob, e.g. *.ts' },
|
|
14
15
|
},
|
|
15
16
|
required: ['pattern'],
|
|
16
17
|
},
|
|
@@ -8,14 +8,17 @@ import { webSearchTool } from './web-search.js';
|
|
|
8
8
|
import { webFetchTool } from './web-fetch.js';
|
|
9
9
|
import { useSkillTool } from './use-skill.js';
|
|
10
10
|
import { askHumanTool } from './ask-human.js';
|
|
11
|
+
import { codegraphTool } from './codegraph.js';
|
|
12
|
+
import { switchModeTool } from './switch-mode.js';
|
|
11
13
|
import { memorySaveTool } from './memory-save.js';
|
|
12
14
|
import { memorySearchTool } from './memory-search.js';
|
|
13
15
|
import { memoryListTool } from './memory-list.js';
|
|
14
16
|
import { memoryUpdateTool } from './memory-update.js';
|
|
15
17
|
import { memoryForgetTool } from './memory-forget.js';
|
|
18
|
+
import { taskTool } from './task.js';
|
|
16
19
|
/**
|
|
17
20
|
* 所有内置工具,按注册顺序排列。
|
|
18
|
-
* 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool
|
|
21
|
+
* 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。无需改 agent / llm。
|
|
19
22
|
*/
|
|
20
23
|
export const builtinTools = [
|
|
21
24
|
readFileTool,
|
|
@@ -24,13 +27,16 @@ export const builtinTools = [
|
|
|
24
27
|
runCommandTool,
|
|
25
28
|
globTool,
|
|
26
29
|
grepTool,
|
|
30
|
+
codegraphTool,
|
|
27
31
|
webSearchTool,
|
|
28
32
|
webFetchTool,
|
|
29
33
|
useSkillTool,
|
|
30
34
|
askHumanTool,
|
|
35
|
+
switchModeTool, // plan↔auto 自切(两模式都可见,不进 PLAN_DISABLED_TOOLS;副作用控制工具→串行分支)
|
|
31
36
|
memorySaveTool,
|
|
32
37
|
memorySearchTool,
|
|
33
38
|
memoryListTool,
|
|
34
39
|
memoryUpdateTool,
|
|
35
40
|
memoryForgetTool,
|
|
41
|
+
taskTool, // 派生子 agent(独立 history + 可受限工具集);plan 模式禁用(见 PLAN_DISABLED_TOOLS)
|
|
36
42
|
];
|
|
@@ -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: '
|
|
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).',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
@@ -11,7 +11,7 @@ export const memoryForgetTool = {
|
|
|
11
11
|
mode: {
|
|
12
12
|
type: 'string',
|
|
13
13
|
enum: ['archive', 'delete'],
|
|
14
|
-
description: '
|
|
14
|
+
description: 'Default archive',
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
17
|
required: ['id'],
|
|
@@ -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: '
|
|
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.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
@@ -14,7 +14,7 @@ export const memoryListTool = {
|
|
|
14
14
|
status: {
|
|
15
15
|
type: 'string',
|
|
16
16
|
enum: ['active', 'superseded', 'archived', 'any'],
|
|
17
|
-
description: '
|
|
17
|
+
description: 'Default active',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
},
|
|
@@ -4,23 +4,23 @@ import { saveEntry } from '../../memory/store.js';
|
|
|
4
4
|
// 撞库(name→id 已存在)拒绝,引导用 memory_update。
|
|
5
5
|
export const memorySaveTool = {
|
|
6
6
|
name: 'memory_save',
|
|
7
|
-
description: '
|
|
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.',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
11
|
-
name: { type: 'string', description: '
|
|
12
|
-
summary: { type: 'string', description: '
|
|
13
|
-
body: { type: 'string', description: '
|
|
11
|
+
name: { type: 'string', description: 'Short unique title (converted to id; rejected if it collides with an existing one)' },
|
|
12
|
+
summary: { type: 'string', description: 'One-line summary (goes into the index; keep it short)' },
|
|
13
|
+
body: { type: 'string', description: 'Full content (details/context/evidence)' },
|
|
14
14
|
type: {
|
|
15
15
|
type: 'string',
|
|
16
16
|
enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
|
|
17
|
-
description: '
|
|
17
|
+
description: 'Category, default fact',
|
|
18
18
|
},
|
|
19
|
-
pinned: { type: 'boolean', description: '
|
|
19
|
+
pinned: { type: 'boolean', description: 'Pin (exempt from auto-forgetting decay), default false' },
|
|
20
20
|
scope: {
|
|
21
21
|
type: 'string',
|
|
22
22
|
enum: ['project', 'global'],
|
|
23
|
-
description: '
|
|
23
|
+
description: 'Store at project level (<cwd>/.mocode/) or global (~/.mocode/), default project',
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
required: ['name', 'summary', 'body'],
|
|
@@ -4,11 +4,11 @@ import { searchEntries } from '../../memory/store.js';
|
|
|
4
4
|
// 结果走 capToolResultForHistory 的放宽上限(同 use_skill,保正文完整)。
|
|
5
5
|
export const memorySearchTool = {
|
|
6
6
|
name: 'memory_search',
|
|
7
|
-
description: '
|
|
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).',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
11
|
-
query: { type: 'string', description: '
|
|
11
|
+
query: { type: 'string', description: 'Keywords (words in id or name work best)' },
|
|
12
12
|
type: {
|
|
13
13
|
type: 'string',
|
|
14
14
|
enum: ['decision', 'fact', 'pitfall', 'reference', 'feedback'],
|
|
@@ -16,9 +16,9 @@ export const memorySearchTool = {
|
|
|
16
16
|
status: {
|
|
17
17
|
type: 'string',
|
|
18
18
|
enum: ['active', 'superseded', 'archived', 'any'],
|
|
19
|
-
description: '
|
|
19
|
+
description: 'Default active (only search live entries)',
|
|
20
20
|
},
|
|
21
|
-
limit: { type: 'integer', description: '
|
|
21
|
+
limit: { type: 'integer', description: 'Number of entries to return, default 5, max 20' },
|
|
22
22
|
},
|
|
23
23
|
required: ['query'],
|
|
24
24
|
},
|
|
@@ -3,16 +3,16 @@ import { updateEntry } from '../../memory/store.js';
|
|
|
3
3
|
// 原地改一条记忆(id 不变)。反思的弱意义:干活时发现事实变了/过时即纠正。
|
|
4
4
|
export const memoryUpdateTool = {
|
|
5
5
|
name: 'memory_update',
|
|
6
|
-
description: '
|
|
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.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
10
10
|
id: { type: 'string' },
|
|
11
|
-
summary: { type: 'string', description: '
|
|
12
|
-
body: { type: 'string', description: '
|
|
13
|
-
name: { type: 'string', description: '
|
|
14
|
-
pinned: { type: 'boolean', description: '
|
|
15
|
-
reason: { type: 'string', description: '
|
|
11
|
+
summary: { type: 'string', description: 'New summary (only pass fields to change)' },
|
|
12
|
+
body: { type: 'string', description: 'New body (only pass fields to change)' },
|
|
13
|
+
name: { type: 'string', description: 'New title (id does not change with name)' },
|
|
14
|
+
pinned: { type: 'boolean', description: 'Toggle pin status (true pins and exempts decay / false unpins)' },
|
|
15
|
+
reason: { type: 'string', description: 'Update reason (recorded in lastUpdateReason for traceability)' },
|
|
16
16
|
},
|
|
17
17
|
required: ['id'],
|
|
18
18
|
},
|
|
@@ -4,13 +4,14 @@ import { MAX_FILE_LINES } from '../constants.js';
|
|
|
4
4
|
// ---------- read_file ----------
|
|
5
5
|
export const readFileTool = {
|
|
6
6
|
name: 'read_file',
|
|
7
|
-
description: '
|
|
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.',
|
|
8
9
|
parameters: {
|
|
9
10
|
type: 'object',
|
|
10
11
|
properties: {
|
|
11
|
-
path: { type: 'string', description: '
|
|
12
|
-
offset: { type: 'integer', description: '
|
|
13
|
-
limit: { type: 'integer', description: '
|
|
12
|
+
path: { type: 'string', description: 'File path, relative to the working directory' },
|
|
13
|
+
offset: { type: 'integer', description: 'Start line (1-based), default 1' },
|
|
14
|
+
limit: { type: 'integer', description: 'Max number of lines to read, default 2000' },
|
|
14
15
|
},
|
|
15
16
|
required: ['path'],
|
|
16
17
|
},
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { MAX_OUTPUT } from '../constants.js';
|
|
3
3
|
// ---------- run_command ----------
|
|
4
4
|
export const runCommandTool = {
|
|
5
5
|
name: 'run_command',
|
|
6
|
-
description: '
|
|
6
|
+
description: 'Execute a shell command, returning combined stdout+stderr. Default timeout 120s. Use for running tests, builds, git, etc.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
10
|
-
command: { type: 'string', description: '
|
|
11
|
-
timeout: { type: 'integer', description: '
|
|
10
|
+
command: { type: 'string', description: 'Command to execute (single line)' },
|
|
11
|
+
timeout: { type: 'integer', description: 'Timeout in milliseconds, default 120000' },
|
|
12
12
|
},
|
|
13
13
|
required: ['command'],
|
|
14
14
|
},
|
|
15
|
-
async execute(args) {
|
|
15
|
+
async execute(args, ctx) {
|
|
16
16
|
const command = String(args.command);
|
|
17
17
|
const timeout = Number(args.timeout ?? 120000);
|
|
18
18
|
return new Promise((done) => {
|
|
@@ -20,11 +20,37 @@ export const runCommandTool = {
|
|
|
20
20
|
const child = spawn(isWin ? 'cmd.exe' : 'bash', isWin ? ['/c', command] : ['-c', command], { cwd: process.cwd() });
|
|
21
21
|
let out = '';
|
|
22
22
|
let finished = false;
|
|
23
|
+
let timer;
|
|
24
|
+
// 杀整棵进程树。child.kill() 在 Windows 只杀 cmd.exe、npm 等子进程会孤儿继续跑(占锁、污染下一步),
|
|
25
|
+
// 故 Win 用 taskkill /T /F 树杀;Unix child.kill('SIGTERM')(bash -c 通常转发给前台子进程,best-effort)。
|
|
26
|
+
const killTree = () => {
|
|
27
|
+
try {
|
|
28
|
+
if (isWin) {
|
|
29
|
+
if (child.pid != null) {
|
|
30
|
+
spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
|
|
31
|
+
stdio: 'ignore',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
child.kill('SIGTERM');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// 进程已退出 / kill 失败:忽略(close 事件会兜底 finish)
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
// abort(用户 Ctrl+C,经 executeTool ctx.signal 透传)→ 杀子进程树 + 返[已中断]
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
killTree();
|
|
46
|
+
finish(`[已中断]\n${out.trim()}`);
|
|
47
|
+
};
|
|
23
48
|
const finish = (s) => {
|
|
24
49
|
if (finished)
|
|
25
50
|
return;
|
|
26
51
|
finished = true;
|
|
27
52
|
clearTimeout(timer);
|
|
53
|
+
ctx?.signal?.removeEventListener('abort', onAbort);
|
|
28
54
|
done(s);
|
|
29
55
|
};
|
|
30
56
|
const onChunk = (chunk) => {
|
|
@@ -40,10 +66,17 @@ export const runCommandTool = {
|
|
|
40
66
|
r += '\n...(输出已截断)';
|
|
41
67
|
finish(`[退出码 ${code}]\n${r || '(无输出)'}`);
|
|
42
68
|
});
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
timer = setTimeout(() => {
|
|
70
|
+
killTree();
|
|
45
71
|
finish(`[超时,已终止]\n${out.trim()}`);
|
|
46
72
|
}, timeout);
|
|
73
|
+
// 外部 abort signal:已 aborted 即时杀(防御;agent 循环顶检查通常会先拦),否则挂监听
|
|
74
|
+
if (ctx?.signal) {
|
|
75
|
+
if (ctx.signal.aborted)
|
|
76
|
+
onAbort();
|
|
77
|
+
else
|
|
78
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
79
|
+
}
|
|
47
80
|
});
|
|
48
81
|
},
|
|
49
82
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { setAgentMode, getAgentMode } from '../../agent/mode.js';
|
|
2
|
+
// ---------- switch_mode ----------
|
|
3
|
+
export const switchModeTool = {
|
|
4
|
+
name: 'switch_mode',
|
|
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.',
|
|
11
|
+
].join(' '),
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
mode: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
enum: ['auto', 'plan'],
|
|
18
|
+
description: 'Target mode: "plan" (read-only) or "auto" (full tools)',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ['mode'],
|
|
22
|
+
},
|
|
23
|
+
async execute(args) {
|
|
24
|
+
const target = String(args.mode);
|
|
25
|
+
if (target !== 'auto' && target !== 'plan') {
|
|
26
|
+
return `错误:mode 必须是 "auto" 或 "plan",收到 "${target}"`;
|
|
27
|
+
}
|
|
28
|
+
const prev = getAgentMode();
|
|
29
|
+
if (prev === target) {
|
|
30
|
+
return `已在 ${target} 模式,无需切换。`;
|
|
31
|
+
}
|
|
32
|
+
setAgentMode(target); // 触发 listener:repl 重写 history[0] 系统提示 + 刷状态行 modeTag
|
|
33
|
+
if (target === 'auto') {
|
|
34
|
+
return [
|
|
35
|
+
`已从 plan 切换到 auto 模式。`,
|
|
36
|
+
`写盘 / 命令 / 记忆写入工具(write_file / edit_file / run_command / memory_save / memory_update / memory_forget)现已恢复可用。`,
|
|
37
|
+
`请按你刚才产出的计划继续执行(同一轮内,无需重述计划)。`,
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
return [
|
|
41
|
+
`已从 auto 切换到 plan 模式(只读探查)。`,
|
|
42
|
+
`写盘 / 命令 / 记忆写入工具已禁用;只用 read_file / glob / grep / codegraph / web_search / web_fetch / use_skill / ask_human / memory_search / memory_list 探查。`,
|
|
43
|
+
`产出计划后 STOP 等用户审批,或(若用户要求自主执行)再调 switch_mode("auto") 续跑。`,
|
|
44
|
+
].join('\n');
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { spawnAgent } from '../../agent/spawn.js';
|
|
2
|
+
import { MAX_OUTPUT } from '../constants.js';
|
|
3
|
+
// ---------- task ----------
|
|
4
|
+
// 派生子 agent 执行独立子任务。子 agent 有独立 history(不污染主对话),
|
|
5
|
+
// 可受限工具子集 + 低步数上限,最终摘要回灌主 history 供主 agent 继续。
|
|
6
|
+
//
|
|
7
|
+
// 适用:并行探查多个文件 / 分而治之的复杂任务 / 隔离上下文避免子任务工具噪声撑爆主窗口。
|
|
8
|
+
// 子 agent 中间过程不写主屏,只返回最终摘要;主 agent 据摘要决定下一步。
|
|
9
|
+
export const taskTool = {
|
|
10
|
+
name: 'task',
|
|
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.',
|
|
16
|
+
].join(''),
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
prompt: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'The sub-task instruction for the sub-agent. Be specific about what to investigate/implement and what to report back.',
|
|
23
|
+
},
|
|
24
|
+
tools: {
|
|
25
|
+
type: 'array',
|
|
26
|
+
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.',
|
|
28
|
+
},
|
|
29
|
+
maxSteps: {
|
|
30
|
+
type: 'number',
|
|
31
|
+
description: 'Optional step limit for the sub-agent (default 50). Lower if the sub-task should be quick.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['prompt'],
|
|
35
|
+
},
|
|
36
|
+
async execute(args, ctx) {
|
|
37
|
+
const prompt = String(args.prompt ?? '');
|
|
38
|
+
if (!prompt)
|
|
39
|
+
return '错误:缺少 prompt(子任务指令)。';
|
|
40
|
+
const tools = Array.isArray(args.tools)
|
|
41
|
+
? args.tools.map((t) => String(t))
|
|
42
|
+
: undefined;
|
|
43
|
+
const maxSteps = typeof args.maxSteps === 'number' && args.maxSteps > 0
|
|
44
|
+
? Math.floor(args.maxSteps)
|
|
45
|
+
: undefined;
|
|
46
|
+
// 透传主 agent 的 abort signal:主 Ctrl+C 树杀子 agent(chat abort + 工具 abort)。
|
|
47
|
+
const result = await spawnAgent({ prompt, tools, maxSteps, signal: ctx?.signal });
|
|
48
|
+
if (!result.completed) {
|
|
49
|
+
return '子 agent 被中断,未完成。';
|
|
50
|
+
}
|
|
51
|
+
if (!result.summary) {
|
|
52
|
+
return '子 agent 完成但未返回文本摘要(可能只调了工具或达到步数上限)。';
|
|
53
|
+
}
|
|
54
|
+
// 摘要可能很长,截到 MAX_OUTPUT 保主 history 不爆。
|
|
55
|
+
const summary = result.summary;
|
|
56
|
+
if (summary.length > MAX_OUTPUT) {
|
|
57
|
+
return (summary.slice(0, MAX_OUTPUT) +
|
|
58
|
+
`\n\n…(子 agent 摘要已截断 ${summary.length - MAX_OUTPUT} 字符)`);
|
|
59
|
+
}
|
|
60
|
+
return summary;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -4,13 +4,13 @@ import { getSkillBody } from '../../skills/index.js';
|
|
|
4
4
|
// 系统提示里已列出可用 skill 的 name + description(何时用),模型据此决定调用。
|
|
5
5
|
export const useSkillTool = {
|
|
6
6
|
name: 'use_skill',
|
|
7
|
-
description: '
|
|
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.',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
11
11
|
name: {
|
|
12
12
|
type: 'string',
|
|
13
|
-
description: '
|
|
13
|
+
description: 'Name of the skill to load (see the skill list in the system prompt, or the /skills command)',
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
required: ['name'],
|
|
@@ -4,15 +4,15 @@ 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: '
|
|
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.',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
11
|
-
url: { type: 'string', description: '
|
|
11
|
+
url: { type: 'string', description: 'Full URL to fetch; must be http/https' },
|
|
12
12
|
},
|
|
13
13
|
required: ['url'],
|
|
14
14
|
},
|
|
15
|
-
async execute(args) {
|
|
15
|
+
async execute(args, ctx) {
|
|
16
16
|
const rawUrl = String(args.url ?? '').trim();
|
|
17
17
|
if (!rawUrl)
|
|
18
18
|
return '错误:url 不能为空。';
|
|
@@ -28,6 +28,15 @@ export const webFetchTool = {
|
|
|
28
28
|
}
|
|
29
29
|
const ctrl = new AbortController();
|
|
30
30
|
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
31
|
+
// 外部 abort signal(用户 Ctrl+C,经 executeTool ctx.signal 透传)→ 合并到 ctrl,fetch 即时取消(不等 30s 超时)
|
|
32
|
+
const onExternalAbort = () => ctrl.abort();
|
|
33
|
+
const externalSignal = ctx?.signal;
|
|
34
|
+
if (externalSignal) {
|
|
35
|
+
if (externalSignal.aborted)
|
|
36
|
+
ctrl.abort();
|
|
37
|
+
else
|
|
38
|
+
externalSignal.addEventListener('abort', onExternalAbort, { once: true });
|
|
39
|
+
}
|
|
31
40
|
try {
|
|
32
41
|
const resp = await fetch(url.href, {
|
|
33
42
|
method: 'GET',
|
|
@@ -56,6 +65,8 @@ export const webFetchTool = {
|
|
|
56
65
|
}
|
|
57
66
|
catch (e) {
|
|
58
67
|
if (ctrl.signal.aborted) {
|
|
68
|
+
if (externalSignal?.aborted)
|
|
69
|
+
return `错误:已中断: ${url.href}`;
|
|
59
70
|
return `错误:抓取超时(${FETCH_TIMEOUT_MS}ms): ${url.href}`;
|
|
60
71
|
}
|
|
61
72
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -63,6 +74,8 @@ export const webFetchTool = {
|
|
|
63
74
|
}
|
|
64
75
|
finally {
|
|
65
76
|
clearTimeout(timer);
|
|
77
|
+
if (externalSignal)
|
|
78
|
+
externalSignal.removeEventListener('abort', onExternalAbort);
|
|
66
79
|
}
|
|
67
80
|
},
|
|
68
81
|
};
|
|
@@ -6,20 +6,20 @@ const MAX_CONTENT_CHARS = 800;
|
|
|
6
6
|
// ---------- web_search ----------
|
|
7
7
|
export const webSearchTool = {
|
|
8
8
|
name: 'web_search',
|
|
9
|
-
description: '
|
|
9
|
+
description: 'Search the web (AnySearch). Returns title/url/snippet/body for each result. Optional tag to switch sub-domain capability (see tag param).',
|
|
10
10
|
parameters: {
|
|
11
11
|
type: 'object',
|
|
12
12
|
properties: {
|
|
13
|
-
query: { type: 'string', description: '
|
|
14
|
-
max_results: { type: 'integer', description: '
|
|
13
|
+
query: { type: 'string', description: 'Search query' },
|
|
14
|
+
max_results: { type: 'integer', description: 'Number of results, 1-20, default 10' },
|
|
15
15
|
tag: {
|
|
16
16
|
type: 'string',
|
|
17
|
-
description: '
|
|
17
|
+
description: 'Sub-domain capability tag, e.g. general.general / code.doc / code.snippet; omit for general search',
|
|
18
18
|
},
|
|
19
|
-
language: { type: 'string', description: '
|
|
19
|
+
language: { type: 'string', description: 'Preferred language, e.g. zh-CN / en, default zh-CN' },
|
|
20
20
|
params: {
|
|
21
21
|
type: 'object',
|
|
22
|
-
description: '
|
|
22
|
+
description: 'Extended params for a specific tag, e.g. {"library":"golang"} for code.doc; not needed for general search',
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
required: ['query'],
|