mocode-ai 1.3.9 → 1.4.0
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 +78 -70
- package/README.zh-CN.md +81 -73
- package/bin/mocode-agent-host.js +1 -1
- package/dist/agent/core.js +226 -293
- package/dist/agent/index.js +14 -10
- package/dist/agent/mode.js +3 -3
- package/dist/agent/runtime-context.js +45 -0
- package/dist/agent/spawn.js +101 -86
- package/dist/agent/tool-helpers.js +134 -0
- package/dist/agent/trace-state.js +118 -0
- package/dist/agents/coordinator.js +6 -1
- package/dist/changeset/index.js +9 -8
- package/dist/config/index.js +109 -77
- package/dist/config/presets.js +3 -1
- package/dist/config/profiles.js +125 -0
- package/dist/context/age-aware.js +2 -4
- package/dist/context/artifacts.js +6 -13
- package/dist/context/budget.js +6 -8
- package/dist/context/classifier.js +1 -2
- package/dist/context/encoders/code.js +2 -6
- package/dist/context/encoders/command.js +1 -3
- package/dist/context/encoders/search.js +1 -3
- package/dist/context/index.js +1 -1
- package/dist/context/lifecycle.js +5 -3
- package/dist/context/pipeline.js +1 -1
- package/dist/context/relevance.js +2 -4
- package/dist/context/token-calibration.js +13 -18
- package/dist/host/stdio.js +73 -12
- package/dist/i18n/index.js +96 -44
- package/dist/index.js +1 -1
- package/dist/llm/index.js +39 -23
- package/dist/llm/provider.js +23 -0
- package/dist/llm/providers/anthropic.js +12 -18
- package/dist/llm/tool-schema.js +48 -0
- package/dist/mcp/client.js +70 -25
- package/dist/mcp/config.js +1 -3
- package/dist/mcp/index.js +3 -1
- package/dist/memory/reflect.js +3 -6
- package/dist/memory/store.js +10 -12
- package/dist/permissions/index.js +66 -22
- package/dist/pet/bridge.js +2 -2
- package/dist/repl/commands/appearance.js +72 -0
- package/dist/repl/commands/compact.js +114 -0
- package/dist/repl/commands/context.js +17 -0
- package/dist/repl/commands/image.js +71 -0
- package/dist/repl/commands/memory.js +51 -0
- package/dist/repl/commands/mode.js +47 -0
- package/dist/repl/commands/model.js +435 -0
- package/dist/repl/commands/pet.js +70 -0
- package/dist/repl/commands/registry.js +46 -0
- package/dist/repl/commands/session.js +102 -0
- package/dist/repl/commands/skill.js +95 -0
- package/dist/repl/commands/system.js +202 -0
- package/dist/repl/commands/tool-group.js +233 -0
- package/dist/repl/commands/types.js +4 -0
- package/dist/repl/commands.js +324 -0
- package/dist/repl/index.js +5 -2577
- package/dist/repl/message-format.js +151 -0
- package/dist/repl/running-input.js +147 -0
- package/dist/repl/runtime.js +816 -0
- package/dist/repl/status-bar.js +161 -0
- package/dist/rollback/index.js +44 -14
- package/dist/runtime/browser-manager.js +1 -1
- package/dist/runtime/dev-server-manager.js +1 -1
- package/dist/runtime/input-injector.js +313 -0
- package/dist/runtime/screen-capture.js +89 -0
- package/dist/runtime/screen-pipeline.js +223 -0
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +8 -2
- package/dist/session/compact.js +22 -34
- package/dist/session/index.js +2 -2
- package/dist/session/notes.js +39 -5
- package/dist/session/persist.js +14 -8
- package/dist/session/scheduler.js +3 -4
- package/dist/session/trace-metrics.js +6 -3
- package/dist/skills/activation.js +10 -5
- package/dist/skills/discover.js +5 -17
- package/dist/skills/index.js +1 -1
- package/dist/skills/runner.js +28 -44
- package/dist/skills/toolmap.js +5 -2
- package/dist/tools/builtins/browser.js +17 -3
- package/dist/tools/builtins/computer.js +322 -0
- package/dist/tools/builtins/dev-server.js +1 -3
- package/dist/tools/builtins/edit-file.js +21 -7
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +28 -25
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-graph.js +4 -7
- package/dist/tools/builtins/memory-list.js +2 -6
- package/dist/tools/builtins/memory-save.js +8 -2
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/note-append.js +14 -3
- package/dist/tools/builtins/plan-update.js +40 -9
- package/dist/tools/builtins/run-command.js +8 -8
- package/dist/tools/builtins/screenshot.js +11 -87
- package/dist/tools/builtins/task.js +29 -40
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/view-image.js +5 -3
- package/dist/tools/builtins/web-fetch.js +1 -3
- package/dist/tools/builtins/web-search.js +3 -7
- package/dist/tools/builtins/write-file.js +4 -2
- package/dist/tools/constants.js +50 -37
- package/dist/tools/policy.js +221 -0
- package/dist/tools/registry.js +40 -19
- package/dist/tools/resource-lock.js +4 -2
- package/dist/tools/router.js +143 -0
- package/dist/tools/validation.js +6 -5
- package/dist/ui/batch.js +63 -23
- package/dist/ui/clipboard.js +17 -5
- package/dist/ui/content.js +166 -13
- package/dist/ui/diff.js +61 -19
- package/dist/ui/fuzzy.js +35 -2
- package/dist/ui/intervention.js +2 -4
- package/dist/ui/layout-internal/content-write.js +587 -0
- package/dist/ui/layout-internal/core.js +590 -0
- package/dist/ui/layout-internal/input-paint.js +365 -0
- package/dist/ui/layout-internal/screen.js +128 -0
- package/dist/ui/layout-internal/scroll.js +152 -0
- package/dist/ui/layout-internal/selection.js +229 -0
- package/dist/ui/layout-internal/state.js +82 -0
- package/dist/ui/layout-internal/statusbar.js +351 -0
- package/dist/ui/layout-types.js +1 -0
- package/dist/ui/layout.js +3 -2288
- package/dist/ui/markdown.js +10 -2
- package/dist/ui/prompt-internal/editor.js +945 -0
- package/dist/ui/prompt-internal/paste.js +52 -0
- package/dist/ui/prompt-internal/pickers.js +519 -0
- package/dist/ui/prompt-internal/types.js +1 -0
- package/dist/ui/prompt.js +4 -1506
- package/dist/ui/render.js +87 -2
- package/dist/verification/affected.js +11 -5
- package/dist/verification/discovery.js +1 -3
- package/dist/verification/profile.js +11 -3
- package/package.json +11 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadImageAttachment, MAX_INLINE_BYTES_DEFAULT
|
|
1
|
+
import { loadImageAttachment, MAX_INLINE_BYTES_DEFAULT } from '../../attachments/image.js';
|
|
2
2
|
export const viewImageTool = {
|
|
3
3
|
name: 'view_image',
|
|
4
4
|
description: 'View a local image as visual model input. Use this whenever the user refers to a screenshot, UI error, design mockup, Figma export, diagram, or other image file. Supports PNG, JPEG, GIF, and WebP up to 4 MiB.',
|
|
@@ -37,13 +37,15 @@ export const viewImageTool = {
|
|
|
37
37
|
code: 'OK',
|
|
38
38
|
retryable: false,
|
|
39
39
|
output: `Viewed image "${att.name}" (${att.mime}, ${att.bytes} bytes). Visual content is attached to the next model request.`,
|
|
40
|
-
modelAttachments: [
|
|
40
|
+
modelAttachments: [
|
|
41
|
+
{
|
|
41
42
|
type: 'image',
|
|
42
43
|
name: att.name,
|
|
43
44
|
mime: att.mime,
|
|
44
45
|
dataUrl: att.dataUrl,
|
|
45
46
|
detail,
|
|
46
|
-
}
|
|
47
|
+
},
|
|
48
|
+
],
|
|
47
49
|
};
|
|
48
50
|
},
|
|
49
51
|
};
|
|
@@ -56,9 +56,7 @@ export const webFetchTool = {
|
|
|
56
56
|
output: `错误:抓取失败 HTTP ${resp.status} ${resp.statusText}\n${text.slice(0, 500)}`,
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
-
const isHtml = /html/i.test(contentType) ||
|
|
60
|
-
/^\s*<!doctype html/i.test(text) ||
|
|
61
|
-
/<html[\s>]/i.test(text.slice(0, 1000));
|
|
59
|
+
const isHtml = /html/i.test(contentType) || /^\s*<!doctype html/i.test(text) || /<html[\s>]/i.test(text.slice(0, 1000));
|
|
62
60
|
const body = isHtml ? htmlToText(text) : text;
|
|
63
61
|
const ct = contentType.split(';')[0].trim();
|
|
64
62
|
const prefix = `${url.href} (HTTP ${resp.status}${ct ? ', ' + ct : ''})\n\n`;
|
|
@@ -37,9 +37,7 @@ export const webSearchTool = {
|
|
|
37
37
|
body.tag = String(args.tag);
|
|
38
38
|
if (args.language)
|
|
39
39
|
body.language = String(args.language);
|
|
40
|
-
if (args.params &&
|
|
41
|
-
typeof args.params === 'object' &&
|
|
42
|
-
!Array.isArray(args.params)) {
|
|
40
|
+
if (args.params && typeof args.params === 'object' && !Array.isArray(args.params)) {
|
|
43
41
|
body.params = args.params;
|
|
44
42
|
}
|
|
45
43
|
const base = config.searchBaseUrl.replace(/\/+$/, '');
|
|
@@ -100,8 +98,7 @@ export const webSearchTool = {
|
|
|
100
98
|
return {
|
|
101
99
|
status: 'error',
|
|
102
100
|
code: !resp.ok || Number(code) === 429 ? 'HTTP_ERROR' : 'EXECUTION_ERROR',
|
|
103
|
-
retryable: resp.status === 408 || resp.status === 429 ||
|
|
104
|
-
resp.status >= 500 || Number(code) === 429,
|
|
101
|
+
retryable: resp.status === 408 || resp.status === 429 || resp.status >= 500 || Number(code) === 429,
|
|
105
102
|
output: `错误:搜索失败 [${code}] ${message}${rid}${hint}`,
|
|
106
103
|
};
|
|
107
104
|
}
|
|
@@ -122,8 +119,7 @@ export const webSearchTool = {
|
|
|
122
119
|
lines.push(` ${snippet}`);
|
|
123
120
|
if (content) {
|
|
124
121
|
const c = content.length > MAX_CONTENT_CHARS
|
|
125
|
-
? content.slice(0, MAX_CONTENT_CHARS) +
|
|
126
|
-
` …(共 ${content.length} 字符)`
|
|
122
|
+
? content.slice(0, MAX_CONTENT_CHARS) + ` …(共 ${content.length} 字符)`
|
|
127
123
|
: content;
|
|
128
124
|
lines.push(` ${c}`);
|
|
129
125
|
}
|
|
@@ -37,12 +37,14 @@ export const writeFileTool = {
|
|
|
37
37
|
return conflict(file, 'expected_hash 必须是 null 或 sha256:<64 hex>。');
|
|
38
38
|
}
|
|
39
39
|
const operation = expectedHash === null ? 'create' : 'update';
|
|
40
|
-
const result = await commitChangeSet(createChangeSet([
|
|
40
|
+
const result = await commitChangeSet(createChangeSet([
|
|
41
|
+
{
|
|
41
42
|
path: file,
|
|
42
43
|
operation,
|
|
43
44
|
expectedHash,
|
|
44
45
|
replacement: content,
|
|
45
|
-
}
|
|
46
|
+
},
|
|
47
|
+
]), ctx?.signal);
|
|
46
48
|
if (result.status === 'conflict') {
|
|
47
49
|
const item = result.conflicts[0];
|
|
48
50
|
return conflict(file, `expected=${item?.expectedHash ?? 'missing'}, actual=${item?.actualHash ?? 'missing'}。请重新读取后再写入。`);
|
package/dist/tools/constants.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** 工具共享的截断 / 上限 / 忽略规则。 */
|
|
2
|
-
import { isMemoryEnabled, isSubAgentEnabled, isFrontendToolsEnabled } from '../config/index.js';
|
|
2
|
+
import { getActiveProfile, isMemoryEnabled, isSubAgentEnabled, isFrontendToolsEnabled, isComputerUseEnabled, } from '../config/index.js';
|
|
3
|
+
import { getProfileToolNames, TOOL_GROUPS } from '../config/profiles.js';
|
|
3
4
|
import { getActiveSkill } from '../skills/activation.js';
|
|
4
5
|
export const MAX_FILE_LINES = 2000;
|
|
5
6
|
export const MAX_OUTPUT = 20000;
|
|
@@ -52,18 +53,33 @@ export const MAX_GRAPH_EDGES = 2000;
|
|
|
52
53
|
// .codegraph:codegraph 索引目录(codegraph.db 是 SQLite 二进制 + daemon.log),
|
|
53
54
|
// grep/glob 扫它无意义且会产出数 KB 的超长「行」,污染 TUI 展开渲染。
|
|
54
55
|
export const IGNORE = ['**/node_modules/**', '**/.git/**', '**/.codegraph/**'];
|
|
55
|
-
// ──
|
|
56
|
+
// ── legacy profile fallback(官方 TUI/stdio 已由 ToolPolicy 控制)────────────────
|
|
57
|
+
// 保留导出名以兼容未传 ToolPolicy 的嵌入调用与旧测试。
|
|
58
|
+
export const FRONTEND_TOOLS = new Set(TOOL_GROUPS.frontend);
|
|
59
|
+
export const COMPUTER_TOOLS = new Set(TOOL_GROUPS.computer);
|
|
56
60
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
61
|
+
* Legacy profile fallback 的屏蔽集合。仅供未传 ToolPolicy 的嵌入调用;官方 TUI/stdio
|
|
62
|
+
* 从每 turn snapshot 构造 schema 并以同一 allow-list 做运行时校验。旧 env 查询在这里
|
|
63
|
+
* 仍保留原有 true/false 覆盖语义,避免破坏兼容调用方。
|
|
60
64
|
*/
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
export function getProfileDisabledTools() {
|
|
66
|
+
const all = new Set(Object.values(TOOL_GROUPS).flat());
|
|
67
|
+
for (const name of getProfileToolNames(getActiveProfile()))
|
|
68
|
+
all.delete(name);
|
|
69
|
+
const reconcile = (enabled, names) => {
|
|
70
|
+
for (const n of names) {
|
|
71
|
+
if (enabled)
|
|
72
|
+
all.delete(n);
|
|
73
|
+
else
|
|
74
|
+
all.add(n);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
reconcile(isMemoryEnabled(), TOOL_GROUPS.memory);
|
|
78
|
+
reconcile(isFrontendToolsEnabled(), TOOL_GROUPS.frontend);
|
|
79
|
+
reconcile(isComputerUseEnabled(), TOOL_GROUPS.computer);
|
|
80
|
+
reconcile(isSubAgentEnabled(), TOOL_GROUPS.subagent);
|
|
81
|
+
return all;
|
|
82
|
+
}
|
|
67
83
|
// ── plan 模式(只读规划,不执行)──────────────────────────────────────────────
|
|
68
84
|
/**
|
|
69
85
|
* plan 模式下从工具 schema 里剔除的工具(模型根本看不到 → 调不到):
|
|
@@ -85,40 +101,25 @@ export const PLAN_DISABLED_TOOLS = new Set([
|
|
|
85
101
|
'memory_graph', // 混合工具(add 写图),plan 只读模式整体屏蔽
|
|
86
102
|
'sub-agent',
|
|
87
103
|
'run_skill', // fork 子 agent 执行面;plan 模式不应派生子工作流
|
|
104
|
+
'computer', // 桌面操控:plan 只读模式无论开关状态都永远屏蔽
|
|
88
105
|
]);
|
|
89
106
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* 调用方(agent/core 串行分支、llm/planChatTools)每次 chat 时调本函数拿当前值。
|
|
107
|
+
* Legacy plan schema 的动态清理:Memory Index 关闭时从旧 profile 屏蔽集删除不存在于该
|
|
108
|
+
* 兼容 schema 的写工具名。自动路由主路径直接对 ToolPolicy snapshot 与 PLAN_DISABLED_TOOLS
|
|
109
|
+
* 求交,不使用本函数决定 memory route eligibility。
|
|
94
110
|
*/
|
|
95
111
|
export function getPlanDisabledTools() {
|
|
96
|
-
const next =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
n.delete('memory_update');
|
|
102
|
-
n.delete('memory_forget');
|
|
103
|
-
return n;
|
|
104
|
-
})();
|
|
105
|
-
// 前端工具簇关闭时,plan 模式 schema 也一并剔除(与 auto 模式一致)。
|
|
106
|
-
if (!isFrontendToolsEnabled()) {
|
|
107
|
-
for (const name of FRONTEND_TOOLS)
|
|
108
|
-
next.add(name);
|
|
112
|
+
const next = new Set(PLAN_DISABLED_TOOLS);
|
|
113
|
+
if (!isMemoryEnabled()) {
|
|
114
|
+
next.delete('memory_save');
|
|
115
|
+
next.delete('memory_update');
|
|
116
|
+
next.delete('memory_forget');
|
|
109
117
|
}
|
|
110
118
|
return next;
|
|
111
119
|
}
|
|
112
|
-
/**
|
|
113
|
-
export function
|
|
120
|
+
/** inline skill 的 disallowed-tools;自动路由 policy 在此基础上继续求交。 */
|
|
121
|
+
export function getSkillRuntimeDisabledTools() {
|
|
114
122
|
const disabled = new Set();
|
|
115
|
-
if (!isSubAgentEnabled())
|
|
116
|
-
disabled.add('sub-agent');
|
|
117
|
-
if (!isFrontendToolsEnabled()) {
|
|
118
|
-
for (const name of FRONTEND_TOOLS)
|
|
119
|
-
disabled.add(name);
|
|
120
|
-
}
|
|
121
|
-
// inline skill 激活态的 disallowed-tools:即便模型幻觉调用也执行不了(设计 §3.6)。
|
|
122
123
|
const active = getActiveSkill();
|
|
123
124
|
if (active?.disallowed) {
|
|
124
125
|
for (const name of active.disallowed)
|
|
@@ -126,3 +127,15 @@ export function getRuntimeDisabledTools() {
|
|
|
126
127
|
}
|
|
127
128
|
return disabled;
|
|
128
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* 旧 profile 兼容路径的运行时功能开关防线。自动路由主路径改由 AgentRunOptions.toolPolicy
|
|
132
|
+
* 提供同源 snapshot;这里只服务尚未迁移的嵌入调用。
|
|
133
|
+
*/
|
|
134
|
+
export function getRuntimeDisabledTools() {
|
|
135
|
+
const disabled = getProfileDisabledTools();
|
|
136
|
+
if (!isSubAgentEnabled())
|
|
137
|
+
disabled.add('sub-agent');
|
|
138
|
+
for (const name of getSkillRuntimeDisabledTools())
|
|
139
|
+
disabled.add(name);
|
|
140
|
+
return disabled;
|
|
141
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { ADD_TOOL_GROUPS_TOOL_NAME, COMMON_TOOL_NAMES, TOOL_ROUTE_GROUP_NAMES, TOOL_ROUTE_GROUPS, getToolRouteGroupNames, isToolRouteGroupName, } from '../config/profiles.js';
|
|
2
|
+
import { PLAN_DISABLED_TOOLS } from './constants.js';
|
|
3
|
+
import { tools } from './registry.js';
|
|
4
|
+
const clampConfidence = (value) => (Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0);
|
|
5
|
+
function envGateAllows(name) {
|
|
6
|
+
return !name || process.env[name] !== 'false';
|
|
7
|
+
}
|
|
8
|
+
function registeredNames() {
|
|
9
|
+
return tools.map((tool) => tool.name);
|
|
10
|
+
}
|
|
11
|
+
/** 当前进程真正可路由的簇;旧 env=false 只作为硬 veto,不再强制把簇常驻 schema。 */
|
|
12
|
+
export function getAvailableToolRouteGroups() {
|
|
13
|
+
const names = registeredNames();
|
|
14
|
+
const registered = new Set(names);
|
|
15
|
+
return TOOL_ROUTE_GROUP_NAMES.filter((group) => {
|
|
16
|
+
const definition = TOOL_ROUTE_GROUPS[group];
|
|
17
|
+
if (!envGateAllows(definition.gateEnv))
|
|
18
|
+
return false;
|
|
19
|
+
const groupNames = getToolRouteGroupNames(group, names);
|
|
20
|
+
if (group === 'mcp')
|
|
21
|
+
return groupNames.length > 0;
|
|
22
|
+
return groupNames.length > 0 && groupNames.every((name) => registered.has(name));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function toolRouteCatalog(groups = getAvailableToolRouteGroups()) {
|
|
26
|
+
const names = registeredNames();
|
|
27
|
+
return groups
|
|
28
|
+
.map((group) => {
|
|
29
|
+
const members = getToolRouteGroupNames(group, names);
|
|
30
|
+
return `- ${group}: ${TOOL_ROUTE_GROUPS[group].description} Tools: ${members.join(', ') || '(dynamic MCP tools)'}.`;
|
|
31
|
+
})
|
|
32
|
+
.join('\n');
|
|
33
|
+
}
|
|
34
|
+
export function getToolChatSchema(name) {
|
|
35
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
36
|
+
if (!tool)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
type: 'function',
|
|
40
|
+
function: {
|
|
41
|
+
name: tool.name,
|
|
42
|
+
description: tool.description,
|
|
43
|
+
parameters: tool.parameters,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function addToolGroupsSchema(groups) {
|
|
48
|
+
return {
|
|
49
|
+
type: 'function',
|
|
50
|
+
function: {
|
|
51
|
+
name: ADD_TOOL_GROUPS_TOOL_NAME,
|
|
52
|
+
description: 'Expand the tool surface for this turn when the current tools are insufficient. Call this tool ALONE, before any dependent tool call. Added groups become available on the next model step and cannot be removed during the turn.',
|
|
53
|
+
parameters: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
groups: {
|
|
57
|
+
type: 'array',
|
|
58
|
+
items: { type: 'string', enum: [...groups] },
|
|
59
|
+
minItems: 1,
|
|
60
|
+
// 不用 uniqueItems:kimi-k3@dashscope 等服务端对含该关键字的工具 schema 整请求 400
|
|
61
|
+
// (InternalError.Algo: Invalid request parameters)。去重语义写进 description,
|
|
62
|
+
// 且 expand() 本身对重复组幂等(已激活的直接 rejected),不依赖 schema 约束。
|
|
63
|
+
description: 'Additional capability groups needed to complete the current task. Do not repeat a group.',
|
|
64
|
+
},
|
|
65
|
+
reason: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Concise evidence-based reason these capabilities are now required.',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ['groups', 'reason'],
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 一个用户 turn 独享的版本化工具策略。每次扩容生成新 snapshot;已发出的模型请求继续用
|
|
78
|
+
* 旧 snapshot 校验其 tool_calls,因此 schema 与执行 backstop 永远同源。
|
|
79
|
+
*/
|
|
80
|
+
export class ToolPolicyController {
|
|
81
|
+
id;
|
|
82
|
+
selected = new Set();
|
|
83
|
+
maxExpansions;
|
|
84
|
+
expansionCount = 0;
|
|
85
|
+
version = 1;
|
|
86
|
+
reason;
|
|
87
|
+
confidence;
|
|
88
|
+
autoCache = null;
|
|
89
|
+
planCache = null;
|
|
90
|
+
constructor(init = {}) {
|
|
91
|
+
this.id = init.id ?? `route-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
92
|
+
this.maxExpansions = Math.max(0, init.maxExpansions ?? 3);
|
|
93
|
+
this.reason = init.reason?.trim() || 'LLM router selected common tools only.';
|
|
94
|
+
this.confidence = clampConfidence(init.confidence ?? 0);
|
|
95
|
+
const available = new Set(getAvailableToolRouteGroups());
|
|
96
|
+
const requested = process.env.MOCODE_TOOL_POLICY === 'full' ? available : new Set(init.groups ?? []);
|
|
97
|
+
for (const group of requested) {
|
|
98
|
+
if (available.has(group))
|
|
99
|
+
this.selected.add(group);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
get groupNames() {
|
|
103
|
+
return TOOL_ROUTE_GROUP_NAMES.filter((group) => this.selected.has(group));
|
|
104
|
+
}
|
|
105
|
+
get canExpand() {
|
|
106
|
+
return this.expansionCount < this.maxExpansions && this.remainingGroups().length > 0;
|
|
107
|
+
}
|
|
108
|
+
remainingGroups() {
|
|
109
|
+
const available = new Set(getAvailableToolRouteGroups());
|
|
110
|
+
return TOOL_ROUTE_GROUP_NAMES.filter((group) => available.has(group) && !this.selected.has(group));
|
|
111
|
+
}
|
|
112
|
+
snapshot(planMode = false) {
|
|
113
|
+
const cached = planMode ? this.planCache : this.autoCache;
|
|
114
|
+
if (cached)
|
|
115
|
+
return cached;
|
|
116
|
+
const orderedNames = [];
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
const append = (name) => {
|
|
119
|
+
if (!seen.has(name)) {
|
|
120
|
+
seen.add(name);
|
|
121
|
+
orderedNames.push(name);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
for (const name of COMMON_TOOL_NAMES)
|
|
125
|
+
append(name);
|
|
126
|
+
const remaining = this.canExpand ? this.remainingGroups() : [];
|
|
127
|
+
if (remaining.length > 0)
|
|
128
|
+
append(ADD_TOOL_GROUPS_TOOL_NAME);
|
|
129
|
+
const registered = registeredNames();
|
|
130
|
+
for (const group of TOOL_ROUTE_GROUP_NAMES) {
|
|
131
|
+
if (!this.selected.has(group))
|
|
132
|
+
continue;
|
|
133
|
+
for (const name of getToolRouteGroupNames(group, registered))
|
|
134
|
+
append(name);
|
|
135
|
+
}
|
|
136
|
+
const visibleNames = planMode
|
|
137
|
+
? orderedNames.filter((name) => name === ADD_TOOL_GROUPS_TOOL_NAME || (!PLAN_DISABLED_TOOLS.has(name) && !name.startsWith('mcp__')))
|
|
138
|
+
: orderedNames;
|
|
139
|
+
const chatTools = visibleNames.flatMap((name) => {
|
|
140
|
+
if (name === ADD_TOOL_GROUPS_TOOL_NAME)
|
|
141
|
+
return [addToolGroupsSchema(remaining)];
|
|
142
|
+
const schema = getToolChatSchema(name);
|
|
143
|
+
return schema ? [schema] : [];
|
|
144
|
+
});
|
|
145
|
+
const snapshot = {
|
|
146
|
+
id: this.id,
|
|
147
|
+
version: this.version,
|
|
148
|
+
groups: new Set(this.groupNames),
|
|
149
|
+
allowedNames: new Set(chatTools.map((tool) => tool.function.name)),
|
|
150
|
+
tools: chatTools,
|
|
151
|
+
reason: this.reason,
|
|
152
|
+
confidence: this.confidence,
|
|
153
|
+
planMode,
|
|
154
|
+
};
|
|
155
|
+
if (planMode)
|
|
156
|
+
this.planCache = snapshot;
|
|
157
|
+
else
|
|
158
|
+
this.autoCache = snapshot;
|
|
159
|
+
return snapshot;
|
|
160
|
+
}
|
|
161
|
+
expand(rawGroups, reason) {
|
|
162
|
+
const rejected = [];
|
|
163
|
+
const added = [];
|
|
164
|
+
if (this.expansionCount >= this.maxExpansions) {
|
|
165
|
+
return {
|
|
166
|
+
added,
|
|
167
|
+
rejected: ['expansion limit reached'],
|
|
168
|
+
snapshot: this.snapshot(false),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const available = new Set(getAvailableToolRouteGroups());
|
|
172
|
+
for (const value of rawGroups) {
|
|
173
|
+
if (!isToolRouteGroupName(value)) {
|
|
174
|
+
rejected.push(`${String(value)}: unknown group`);
|
|
175
|
+
}
|
|
176
|
+
else if (!available.has(value)) {
|
|
177
|
+
rejected.push(`${value}: capability disabled or unavailable`);
|
|
178
|
+
}
|
|
179
|
+
else if (this.selected.has(value)) {
|
|
180
|
+
rejected.push(`${value}: already active`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
this.selected.add(value);
|
|
184
|
+
added.push(value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (added.length > 0) {
|
|
188
|
+
this.expansionCount++;
|
|
189
|
+
this.version++;
|
|
190
|
+
this.reason = reason.trim() || `Main agent added ${added.join(', ')}.`;
|
|
191
|
+
this.confidence = Math.max(this.confidence, 0.8);
|
|
192
|
+
this.autoCache = null;
|
|
193
|
+
this.planCache = null;
|
|
194
|
+
}
|
|
195
|
+
return { added, rejected, snapshot: this.snapshot(false) };
|
|
196
|
+
}
|
|
197
|
+
/** 注入请求尾部,不改写稳定 system prefix。 */
|
|
198
|
+
reminder(planMode = false) {
|
|
199
|
+
const snapshot = this.snapshot(planMode);
|
|
200
|
+
const active = this.groupNames.length ? this.groupNames.join(', ') : '(common only)';
|
|
201
|
+
const remaining = this.remainingGroups();
|
|
202
|
+
const lines = [
|
|
203
|
+
'## Tool route (current turn)',
|
|
204
|
+
`Policy ${snapshot.id} v${snapshot.version}; active groups: ${active}.`,
|
|
205
|
+
`Router reason: ${snapshot.reason}`,
|
|
206
|
+
'Use only the tools currently exposed. If a required capability is missing, call add_tool_groups alone; dependent calls must wait until the next step.',
|
|
207
|
+
];
|
|
208
|
+
if (remaining.length)
|
|
209
|
+
lines.push(`Groups still available: ${remaining.join(', ')}.`);
|
|
210
|
+
if (this.selected.has('computer-control')) {
|
|
211
|
+
lines.push('Computer control: the computer tool drives the real desktop and returns a fresh screenshot after every action; inspect it before acting again and self-correct.', 'Coordinates use a normalized 0-1000 grid. Zoom into tight regions before clicking small or dense targets.', 'Require explicit user intent before destructive or sensitive targets such as submit, payment, credentials, delete, or send; every action still passes the permission gate.');
|
|
212
|
+
}
|
|
213
|
+
if (this.selected.has('memory-read')) {
|
|
214
|
+
lines.push('Memory read: use memory_search for relevant cross-session facts and memory_list only when an index overview is needed; memory_search may also return graph relations.');
|
|
215
|
+
}
|
|
216
|
+
if (this.selected.has('memory-write')) {
|
|
217
|
+
lines.push('Memory write: persist only stable, non-obvious cross-session facts. Search before saving, update instead of duplicating, and archive stale entries; add graph links only when meaningful.');
|
|
218
|
+
}
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
|
221
|
+
}
|
package/dist/tools/registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
// 注意:不顶层 import builtins——会构成 registry → builtins → task(sub-agent) → agent/spawn
|
|
2
|
+
// → agent/core → registry 的模块循环。官方默认工具包由 builtins/index.ts 经 installBuiltinTools 自注册。
|
|
2
3
|
import { beginPathMutation, beginWorkspaceMutation, endPathMutation, endWorkspaceMutation, getCurrentTurnMutationState, } from '../rollback/index.js';
|
|
3
4
|
import { enforceSandbox } from '../sandbox/index.js';
|
|
4
5
|
import { resolveResourceLockRequests, toolResourceLockManager } from './resource-lock.js';
|
|
@@ -8,14 +9,27 @@ import { isToolErrorOutput } from './result.js';
|
|
|
8
9
|
/**
|
|
9
10
|
* 可扩展工具注册表。数组实例始终稳定,使已经持有 tools 引用的 agent/LLM 能看到运行时新增工具。
|
|
10
11
|
* 扩展按 source 替换,MCP 重连或配置刷新不会累积旧工具。
|
|
12
|
+
*
|
|
13
|
+
* 官方默认工具包不顶层 import——由装配方经 installBuiltinTools() 注入(见 builtins/index.ts 自注册)。
|
|
14
|
+
* 这是「framework 与 coding preset 分离」的装配缝:@mocode/tool-system 不含 builtin,builtin 是可替换默认包。
|
|
15
|
+
* 未 install 时 tools 为空数组;install 后 rebuild 合并 builtin + 已注册扩展。
|
|
11
16
|
*/
|
|
12
17
|
const extensions = new Map();
|
|
13
|
-
|
|
18
|
+
let builtinTools = [];
|
|
19
|
+
export const tools = [];
|
|
14
20
|
/** 名字 → 工具 的 O(1) 索引,与 tools 数组在每次 rebuild 时同步重建;findTool 走此索引。 */
|
|
15
21
|
let toolIndex = buildToolIndex(tools);
|
|
16
22
|
function buildToolIndex(list) {
|
|
17
23
|
return new Map(list.map((tool) => [tool.name, tool]));
|
|
18
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* 装配官方默认工具包(coding preset)。幂等:重复 install 覆盖上次,不累积。
|
|
27
|
+
* 由 builtins/index.ts 在模块初始化时调用;入口(CLI/stdio host/eval/测试)只需 import builtins 即完成装配。
|
|
28
|
+
*/
|
|
29
|
+
export function installBuiltinTools(list) {
|
|
30
|
+
builtinTools = list;
|
|
31
|
+
rebuildTools();
|
|
32
|
+
}
|
|
19
33
|
export function registerToolsExtension(sourceOrAdditions, maybeAdditions) {
|
|
20
34
|
const source = typeof sourceOrAdditions === 'string' ? sourceOrAdditions : 'external';
|
|
21
35
|
const additions = typeof sourceOrAdditions === 'string' ? (maybeAdditions ?? []) : sourceOrAdditions;
|
|
@@ -72,9 +86,12 @@ export function isFileMutationTool(name) {
|
|
|
72
86
|
return isFileMutationCapabilities(getToolCapabilities(name));
|
|
73
87
|
}
|
|
74
88
|
function isStructuredOutcome(value) {
|
|
75
|
-
return typeof value === 'object' &&
|
|
76
|
-
|
|
77
|
-
typeof value.
|
|
89
|
+
return (typeof value === 'object' &&
|
|
90
|
+
value !== null &&
|
|
91
|
+
typeof value.status === 'string' &&
|
|
92
|
+
typeof value.code === 'string' &&
|
|
93
|
+
typeof value.retryable === 'boolean' &&
|
|
94
|
+
typeof value.output === 'string');
|
|
78
95
|
}
|
|
79
96
|
function normalizeOutcome(value, durationMs, changedFiles) {
|
|
80
97
|
if (isStructuredOutcome(value)) {
|
|
@@ -111,22 +128,21 @@ function isTransientExecutionError(error) {
|
|
|
111
128
|
const value = error;
|
|
112
129
|
if (value.name === 'AbortError' || value.name === 'APIUserAbortError')
|
|
113
130
|
return false;
|
|
114
|
-
if (value.status === 408 || value.status === 429 ||
|
|
115
|
-
(typeof value.status === 'number' && value.status >= 500))
|
|
131
|
+
if (value.status === 408 || value.status === 429 || (typeof value.status === 'number' && value.status >= 500))
|
|
116
132
|
return true;
|
|
117
|
-
if (['ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'EPIPE']
|
|
118
|
-
.includes(value.code ?? ''))
|
|
133
|
+
if (['ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'EPIPE'].includes(value.code ?? ''))
|
|
119
134
|
return true;
|
|
120
|
-
return value.name === 'APIConnectionError' ||
|
|
135
|
+
return (value.name === 'APIConnectionError' ||
|
|
121
136
|
value.name === 'APIConnectionTimeoutError' ||
|
|
122
|
-
(typeof value.message === 'string' && /\btime(?:d)?\s*out\b|ETIMEDOUT/i.test(value.message));
|
|
137
|
+
(typeof value.message === 'string' && /\btime(?:d)?\s*out\b|ETIMEDOUT/i.test(value.message)));
|
|
123
138
|
}
|
|
124
139
|
function executionErrorOutcome(name, error, startedAt, changedFiles) {
|
|
125
140
|
const transient = isTransientExecutionError(error);
|
|
126
141
|
const value = error;
|
|
127
|
-
const timeout = transient &&
|
|
128
|
-
value?.
|
|
129
|
-
|
|
142
|
+
const timeout = transient &&
|
|
143
|
+
(value?.code === 'ETIMEDOUT' ||
|
|
144
|
+
value?.name === 'APIConnectionTimeoutError' ||
|
|
145
|
+
(error instanceof Error && /\btime(?:d)?\s*out\b|ETIMEDOUT/i.test(error.message)));
|
|
130
146
|
return {
|
|
131
147
|
status: 'error',
|
|
132
148
|
code: timeout ? 'TIMEOUT' : transient ? 'NETWORK_ERROR' : 'EXECUTION_ERROR',
|
|
@@ -153,18 +169,23 @@ async function executeToolOnce(tool, args, signal, opts) {
|
|
|
153
169
|
mutationVersionBefore = mutationBefore.version;
|
|
154
170
|
// Transactional tools own their full write-set capture inside ChangeSet commit.
|
|
155
171
|
const pathCapture = !capabilities.delegatesResourceLocks &&
|
|
156
|
-
isFileMutationCapabilities(capabilities) &&
|
|
172
|
+
isFileMutationCapabilities(capabilities) &&
|
|
173
|
+
typeof args.path === 'string' &&
|
|
174
|
+
args.path
|
|
157
175
|
? beginPathMutation(args.path)
|
|
158
176
|
: null;
|
|
159
177
|
capturedPath = pathCapture?.path;
|
|
160
178
|
// 工作区快照是异步的:它遍历整棵工作树,同步实现会在每次 run_command/MCP 调用前后
|
|
161
179
|
// 阻塞事件循环数秒(TUI 完全冻结)。await 让 spinner / 走时 / 键鼠在扫描期间继续工作。
|
|
162
|
-
const workspaceCapture = capabilities.effect === 'process' || capabilities.effect === 'unknown'
|
|
163
|
-
? await beginWorkspaceMutation()
|
|
164
|
-
: null;
|
|
180
|
+
const workspaceCapture = capabilities.effect === 'process' || capabilities.effect === 'unknown' ? await beginWorkspaceMutation() : null;
|
|
165
181
|
let raw;
|
|
166
182
|
try {
|
|
167
|
-
raw = await tool.execute(args, {
|
|
183
|
+
raw = await tool.execute(args, {
|
|
184
|
+
signal,
|
|
185
|
+
callId: opts?.callId,
|
|
186
|
+
allowedToolNames: opts?.allowedToolNames,
|
|
187
|
+
delegation: opts?.delegation,
|
|
188
|
+
});
|
|
168
189
|
}
|
|
169
190
|
finally {
|
|
170
191
|
if (pathCapture)
|
|
@@ -100,11 +100,13 @@ export function canonicalFileResourceKey(input) {
|
|
|
100
100
|
function modeFor(effect) {
|
|
101
101
|
return effect === 'read' ? 'read' : 'write';
|
|
102
102
|
}
|
|
103
|
-
const workspaceWrite = () => [
|
|
103
|
+
const workspaceWrite = () => [
|
|
104
|
+
{
|
|
104
105
|
key: 'workspace',
|
|
105
106
|
scope: 'workspace',
|
|
106
107
|
mode: 'write',
|
|
107
|
-
}
|
|
108
|
+
},
|
|
109
|
+
];
|
|
108
110
|
/** Resolve declared logical resources. Any ambiguity fails closed to a workspace write lock. */
|
|
109
111
|
export function resolveResourceLockRequests(capabilities, args) {
|
|
110
112
|
if (capabilities.delegatesResourceLocks)
|