mocode-ai 1.4.2 → 1.4.3
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 +13 -1
- package/dist/agent/core.js +14 -936
- package/dist/agent/index.js +37 -13
- package/dist/agent/model-turn.js +218 -0
- package/dist/agent/pipeline.js +18 -0
- package/dist/agent/run-contracts.js +1 -0
- package/dist/agent/run-coordinator.js +758 -0
- package/dist/agent/runtime-context.js +118 -24
- package/dist/agent/spawn.js +11 -7
- package/dist/agent/stages/context-trimmer.js +63 -0
- package/dist/agent/stages/contracts.js +12 -0
- package/dist/agent/stages/history-manager.js +178 -0
- package/dist/agent/stages/legacy-adapters.js +19 -0
- package/dist/agent/stages/model-runner.js +29 -0
- package/dist/agent/stages/run-policy.js +73 -0
- package/dist/agent/stages/tool-dispatcher.js +341 -0
- package/dist/agent/tool-helpers.js +12 -12
- package/dist/agent/tool-turn.js +87 -0
- package/dist/agent/trace-state.js +97 -101
- package/dist/agent/turn-lifecycle.js +110 -0
- package/dist/config/index.js +14 -0
- package/dist/host/stdio.js +101 -40
- package/dist/llm/index.js +51 -35
- package/dist/llm/providers/anthropic.js +16 -10
- package/dist/llm/runtime.js +1 -0
- package/dist/permissions/index.js +21 -5
- package/dist/repl/commands/compact.js +2 -2
- package/dist/repl/commands/session.js +3 -12
- package/dist/repl/message-format.js +5 -0
- package/dist/repl/runtime.js +95 -55
- package/dist/rollback/index.js +29 -624
- package/dist/rollback/store.js +593 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/runtime.js +307 -0
- package/dist/session/compact.js +22 -14
- package/dist/session/index.js +1 -0
- package/dist/session/persist.js +10 -146
- package/dist/session/scheduler.js +28 -16
- package/dist/session/state.js +16 -12
- package/dist/session/store.js +218 -0
- package/dist/session/trace.js +5 -15
- package/dist/tools/policy.js +19 -15
- package/dist/tools/registry.js +21 -229
- package/dist/tools/router.js +5 -3
- package/dist/tools/tool-runtime.js +267 -0
- package/dist/ui/layout-internal/content-write.js +4 -0
- package/package.json +7 -3
package/dist/tools/registry.js
CHANGED
|
@@ -1,260 +1,52 @@
|
|
|
1
|
+
import { ToolRuntime } from './tool-runtime.js';
|
|
1
2
|
// 注意:不顶层 import builtins——会构成 registry → builtins → task(sub-agent) → agent/spawn
|
|
2
3
|
// → agent/core → registry 的模块循环。官方默认工具包由 builtins/index.ts 经 installBuiltinTools 自注册。
|
|
3
|
-
import { beginPathMutation, beginWorkspaceMutation, endPathMutation, endWorkspaceMutation, getCurrentTurnMutationState, } from '../rollback/index.js';
|
|
4
|
-
import { enforceSandbox } from '../sandbox/index.js';
|
|
5
|
-
import { resolveResourceLockRequests, toolResourceLockManager } from './resource-lock.js';
|
|
6
|
-
import { validateToolArguments } from './validation.js';
|
|
7
|
-
import { t } from '../i18n/index.js';
|
|
8
|
-
import { isToolErrorOutput } from './result.js';
|
|
9
4
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 官方默认工具包不顶层 import——由装配方经 installBuiltinTools() 注入(见 builtins/index.ts 自注册)。
|
|
14
|
-
* 这是「framework 与 coding preset 分离」的装配缝:@mocode/tool-system 不含 builtin,builtin 是可替换默认包。
|
|
15
|
-
* 未 install 时 tools 为空数组;install 后 rebuild 合并 builtin + 已注册扩展。
|
|
16
|
-
*/
|
|
17
|
-
const extensions = new Map();
|
|
18
|
-
let builtinTools = [];
|
|
19
|
-
export const tools = [];
|
|
20
|
-
/** 名字 → 工具 的 O(1) 索引,与 tools 数组在每次 rebuild 时同步重建;findTool 走此索引。 */
|
|
21
|
-
let toolIndex = buildToolIndex(tools);
|
|
22
|
-
function buildToolIndex(list) {
|
|
23
|
-
return new Map(list.map((tool) => [tool.name, tool]));
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* 装配官方默认工具包(coding preset)。幂等:重复 install 覆盖上次,不累积。
|
|
27
|
-
* 由 builtins/index.ts 在模块初始化时调用;入口(CLI/stdio host/eval/测试)只需 import builtins 即完成装配。
|
|
5
|
+
* 默认工具运行时。现有模块级 API 均为薄委托;新调用方可创建独立 ToolRuntime。
|
|
6
|
+
* 官方默认工具包不顶层 import,由 builtins/index.ts 经 installBuiltinTools() 注入。
|
|
28
7
|
*/
|
|
8
|
+
export const defaultToolRuntime = new ToolRuntime();
|
|
9
|
+
/** 稳定数组引用;defaultToolRuntime 重建时仅原地 splice。 */
|
|
10
|
+
export const tools = defaultToolRuntime.tools;
|
|
11
|
+
/** 装配官方默认工具包(coding preset)。幂等:重复 install 覆盖上次,不累积。 */
|
|
29
12
|
export function installBuiltinTools(list) {
|
|
30
|
-
|
|
31
|
-
rebuildTools();
|
|
13
|
+
defaultToolRuntime.installBuiltinTools(list);
|
|
32
14
|
}
|
|
33
15
|
export function registerToolsExtension(sourceOrAdditions, maybeAdditions) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const rejected = additions
|
|
38
|
-
.filter((tool) => builtinTools.some((builtin) => builtin.name === tool.name))
|
|
39
|
-
.map((tool) => tool.name);
|
|
40
|
-
rebuildTools();
|
|
41
|
-
return rejected;
|
|
16
|
+
return typeof sourceOrAdditions === 'string'
|
|
17
|
+
? defaultToolRuntime.registerToolsExtension(sourceOrAdditions, maybeAdditions ?? [])
|
|
18
|
+
: defaultToolRuntime.registerToolsExtension(sourceOrAdditions);
|
|
42
19
|
}
|
|
43
20
|
export function clearToolsExtension(source) {
|
|
44
|
-
|
|
45
|
-
rebuildTools();
|
|
21
|
+
defaultToolRuntime.clearToolsExtension(source);
|
|
46
22
|
}
|
|
47
|
-
function rebuildTools() {
|
|
48
|
-
const names = new Set();
|
|
49
|
-
const next = [];
|
|
50
|
-
for (const tool of [...builtinTools, ...Array.from(extensions.values()).flat()]) {
|
|
51
|
-
if (names.has(tool.name))
|
|
52
|
-
continue;
|
|
53
|
-
names.add(tool.name);
|
|
54
|
-
next.push(tool);
|
|
55
|
-
}
|
|
56
|
-
tools.splice(0, tools.length, ...next);
|
|
57
|
-
toolIndex = buildToolIndex(next);
|
|
58
|
-
}
|
|
59
|
-
const DEFAULT_CAPABILITIES = Object.freeze({
|
|
60
|
-
effect: 'unknown',
|
|
61
|
-
concurrency: 'serial',
|
|
62
|
-
});
|
|
63
23
|
export function findTool(name) {
|
|
64
|
-
return
|
|
24
|
+
return defaultToolRuntime.findTool(name);
|
|
65
25
|
}
|
|
66
26
|
/** 缺少声明或找不到工具时返回保守能力,绝不把未知扩展并发执行。 */
|
|
67
27
|
export function getToolCapabilities(toolOrName) {
|
|
68
|
-
|
|
69
|
-
return tool?.capabilities ?? DEFAULT_CAPABILITIES;
|
|
28
|
+
return defaultToolRuntime.getToolCapabilities(toolOrName);
|
|
70
29
|
}
|
|
71
30
|
export function getToolResourceKeys(toolOrName, args) {
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
return capabilities.resources?.(args) ?? [];
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
31
|
+
return defaultToolRuntime.getToolResourceKeys(toolOrName, args);
|
|
79
32
|
}
|
|
80
33
|
/** resource-locked write 是可生成文件 diff/按路径记 rollback 的文件 mutation。 */
|
|
81
34
|
export function isFileMutationCapabilities(capabilities) {
|
|
82
|
-
return
|
|
35
|
+
return defaultToolRuntime.isFileMutationCapabilities(capabilities);
|
|
83
36
|
}
|
|
84
|
-
/** 按工具名判定(兼容入口);热路径请直接复用已解析的 capabilities
|
|
37
|
+
/** 按工具名判定(兼容入口);热路径请直接复用已解析的 capabilities。 */
|
|
85
38
|
export function isFileMutationTool(name) {
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
function isStructuredOutcome(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');
|
|
95
|
-
}
|
|
96
|
-
function normalizeOutcome(value, durationMs, changedFiles) {
|
|
97
|
-
if (isStructuredOutcome(value)) {
|
|
98
|
-
return {
|
|
99
|
-
...value,
|
|
100
|
-
durationMs: value.durationMs ?? durationMs,
|
|
101
|
-
changedFiles: value.changedFiles ?? changedFiles,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
const failed = isToolErrorOutput(value);
|
|
105
|
-
return {
|
|
106
|
-
status: failed ? 'error' : 'success',
|
|
107
|
-
code: failed ? 'EXECUTION_ERROR' : 'OK',
|
|
108
|
-
// Legacy string errors carry no transient classification and are never retried blindly.
|
|
109
|
-
retryable: false,
|
|
110
|
-
output: value,
|
|
111
|
-
changedFiles,
|
|
112
|
-
durationMs,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
function terminalOutcome(status, code, output, startedAt, changedFiles = []) {
|
|
116
|
-
return {
|
|
117
|
-
status,
|
|
118
|
-
code,
|
|
119
|
-
retryable: false,
|
|
120
|
-
output,
|
|
121
|
-
changedFiles,
|
|
122
|
-
durationMs: Date.now() - startedAt,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function isTransientExecutionError(error) {
|
|
126
|
-
if (!error || typeof error !== 'object')
|
|
127
|
-
return false;
|
|
128
|
-
const value = error;
|
|
129
|
-
if (value.name === 'AbortError' || value.name === 'APIUserAbortError')
|
|
130
|
-
return false;
|
|
131
|
-
if (value.status === 408 || value.status === 429 || (typeof value.status === 'number' && value.status >= 500))
|
|
132
|
-
return true;
|
|
133
|
-
if (['ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'EPIPE'].includes(value.code ?? ''))
|
|
134
|
-
return true;
|
|
135
|
-
return (value.name === 'APIConnectionError' ||
|
|
136
|
-
value.name === 'APIConnectionTimeoutError' ||
|
|
137
|
-
(typeof value.message === 'string' && /\btime(?:d)?\s*out\b|ETIMEDOUT/i.test(value.message)));
|
|
138
|
-
}
|
|
139
|
-
function executionErrorOutcome(name, error, startedAt, changedFiles) {
|
|
140
|
-
const transient = isTransientExecutionError(error);
|
|
141
|
-
const value = error;
|
|
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)));
|
|
146
|
-
return {
|
|
147
|
-
status: 'error',
|
|
148
|
-
code: timeout ? 'TIMEOUT' : transient ? 'NETWORK_ERROR' : 'EXECUTION_ERROR',
|
|
149
|
-
retryable: transient,
|
|
150
|
-
output: t('toolError.execution', {
|
|
151
|
-
name,
|
|
152
|
-
message: error instanceof Error ? error.message : String(error),
|
|
153
|
-
}),
|
|
154
|
-
changedFiles,
|
|
155
|
-
durationMs: Date.now() - startedAt,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
/** Execute one tool call while holding its declared locks and capturing rollback state. */
|
|
159
|
-
async function executeToolOnce(tool, args, signal, opts) {
|
|
160
|
-
const startedAt = Date.now();
|
|
161
|
-
const capabilities = getToolCapabilities(tool);
|
|
162
|
-
let mutationVersionBefore;
|
|
163
|
-
let capturedPath;
|
|
164
|
-
try {
|
|
165
|
-
const requests = resolveResourceLockRequests(capabilities, args);
|
|
166
|
-
return await toolResourceLockManager.withLocks(requests, signal, async () => {
|
|
167
|
-
opts?.onLockAcquired?.(args);
|
|
168
|
-
const mutationBefore = getCurrentTurnMutationState();
|
|
169
|
-
mutationVersionBefore = mutationBefore.version;
|
|
170
|
-
// Transactional tools own their full write-set capture inside ChangeSet commit.
|
|
171
|
-
const pathCapture = !capabilities.delegatesResourceLocks &&
|
|
172
|
-
isFileMutationCapabilities(capabilities) &&
|
|
173
|
-
typeof args.path === 'string' &&
|
|
174
|
-
args.path
|
|
175
|
-
? beginPathMutation(args.path)
|
|
176
|
-
: null;
|
|
177
|
-
capturedPath = pathCapture?.path;
|
|
178
|
-
// 工作区快照是异步的:它遍历整棵工作树,同步实现会在每次 run_command/MCP 调用前后
|
|
179
|
-
// 阻塞事件循环数秒(TUI 完全冻结)。await 让 spinner / 走时 / 键鼠在扫描期间继续工作。
|
|
180
|
-
const workspaceCapture = capabilities.effect === 'process' || capabilities.effect === 'unknown' ? await beginWorkspaceMutation() : null;
|
|
181
|
-
let raw;
|
|
182
|
-
try {
|
|
183
|
-
raw = await tool.execute(args, {
|
|
184
|
-
signal,
|
|
185
|
-
callId: opts?.callId,
|
|
186
|
-
allowedToolNames: opts?.allowedToolNames,
|
|
187
|
-
delegation: opts?.delegation,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
finally {
|
|
191
|
-
if (pathCapture)
|
|
192
|
-
endPathMutation(pathCapture, tool.name);
|
|
193
|
-
if (workspaceCapture)
|
|
194
|
-
await endWorkspaceMutation(workspaceCapture, tool.name);
|
|
195
|
-
}
|
|
196
|
-
const mutationAfter = getCurrentTurnMutationState();
|
|
197
|
-
const changedFiles = mutationAfter.version !== mutationBefore.version
|
|
198
|
-
? pathCapture
|
|
199
|
-
? mutationAfter.changedFiles.filter((item) => item.path === pathCapture.path).map((item) => item.path)
|
|
200
|
-
: mutationAfter.changedFiles.map((item) => item.path)
|
|
201
|
-
: [];
|
|
202
|
-
if (signal?.aborted) {
|
|
203
|
-
const aborted = terminalOutcome('aborted', 'ABORTED', String(isStructuredOutcome(raw) ? raw.output : raw), startedAt, changedFiles);
|
|
204
|
-
return isStructuredOutcome(raw) ? { ...aborted, usage: raw.usage } : aborted;
|
|
205
|
-
}
|
|
206
|
-
return normalizeOutcome(raw, Date.now() - startedAt, changedFiles);
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
catch (error) {
|
|
210
|
-
const mutationAfter = getCurrentTurnMutationState();
|
|
211
|
-
const changedFiles = mutationVersionBefore !== undefined && mutationAfter.version !== mutationVersionBefore
|
|
212
|
-
? capturedPath
|
|
213
|
-
? mutationAfter.changedFiles.filter((item) => item.path === capturedPath).map((item) => item.path)
|
|
214
|
-
: mutationAfter.changedFiles.map((item) => item.path)
|
|
215
|
-
: [];
|
|
216
|
-
if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {
|
|
217
|
-
return terminalOutcome('aborted', 'ABORTED', t('command.interrupted'), startedAt, changedFiles);
|
|
218
|
-
}
|
|
219
|
-
return executionErrorOutcome(tool.name, error, startedAt, changedFiles);
|
|
220
|
-
}
|
|
39
|
+
return defaultToolRuntime.isFileMutationTool(name);
|
|
221
40
|
}
|
|
222
41
|
/**
|
|
223
42
|
* 结构化工具调度入口。永不抛错;旧字符串工具在此归一化为 ToolOutcome。
|
|
224
43
|
* 权限仍由 Agent 在展示工具头之前预检,保持现有交互时序。
|
|
225
44
|
*/
|
|
226
45
|
export async function executeToolOutcome(name, argsRaw, signal, opts) {
|
|
227
|
-
|
|
228
|
-
if (signal?.aborted) {
|
|
229
|
-
return terminalOutcome('aborted', 'ABORTED', t('command.interrupted'), startedAt);
|
|
230
|
-
}
|
|
231
|
-
const tool = findTool(name);
|
|
232
|
-
if (!tool) {
|
|
233
|
-
return terminalOutcome('error', 'UNKNOWN_TOOL', t('toolError.unknown', { name }), startedAt);
|
|
234
|
-
}
|
|
235
|
-
let parsed;
|
|
236
|
-
try {
|
|
237
|
-
parsed = argsRaw.trim() ? JSON.parse(argsRaw) : {};
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
return terminalOutcome('error', 'INVALID_JSON', t('toolError.invalidJson', { name, arguments: argsRaw }), startedAt);
|
|
241
|
-
}
|
|
242
|
-
const validation = validateToolArguments(tool, parsed);
|
|
243
|
-
if (!validation.valid) {
|
|
244
|
-
const hint = opts?.argumentErrorHint?.trim();
|
|
245
|
-
const message = hint
|
|
246
|
-
? `错误:工具 ${name} 参数无效: ${validation.message}\n${hint}`
|
|
247
|
-
: `错误:工具 ${name} 参数无效: ${validation.message}`;
|
|
248
|
-
return terminalOutcome('error', validation.code, message, startedAt);
|
|
249
|
-
}
|
|
250
|
-
const args = parsed;
|
|
251
|
-
const sandboxError = enforceSandbox(name, args);
|
|
252
|
-
if (sandboxError) {
|
|
253
|
-
return terminalOutcome('denied', 'SANDBOX_DENIED', sandboxError, startedAt);
|
|
254
|
-
}
|
|
255
|
-
return executeToolOnce(tool, args, signal, opts);
|
|
46
|
+
return defaultToolRuntime.executeToolOutcome(name, argsRaw, signal, opts);
|
|
256
47
|
}
|
|
257
48
|
/** 字符串兼容入口:现有调用方、TUI 和 LLM history 无需同步迁移。 */
|
|
258
49
|
export async function executeTool(name, argsRaw, signal, opts) {
|
|
259
|
-
return (
|
|
50
|
+
return defaultToolRuntime.executeTool(name, argsRaw, signal, opts);
|
|
260
51
|
}
|
|
52
|
+
export { ToolRuntime } from './tool-runtime.js';
|
package/dist/tools/router.js
CHANGED
|
@@ -86,7 +86,7 @@ function parseDecision(raw, available, previousGroups, startedAt) {
|
|
|
86
86
|
*/
|
|
87
87
|
export async function routeToolGroups(request) {
|
|
88
88
|
const startedAt = Date.now();
|
|
89
|
-
const availableGroups = getAvailableToolRouteGroups();
|
|
89
|
+
const availableGroups = getAvailableToolRouteGroups(request.tools, request.gateAllows);
|
|
90
90
|
const available = new Set(availableGroups);
|
|
91
91
|
const previousGroups = (request.previousGroups ?? []).filter((group) => available.has(group));
|
|
92
92
|
if (availableGroups.length === 0) {
|
|
@@ -98,7 +98,7 @@ Select the minimum sufficient set of capability groups for the user's NEXT agent
|
|
|
98
98
|
Always-available common tools: ${COMMON_TOOL_NAMES.join(', ')}.
|
|
99
99
|
|
|
100
100
|
Available groups:
|
|
101
|
-
${toolRouteCatalog(availableGroups)}
|
|
101
|
+
${request.tools ? toolRouteCatalog(availableGroups, request.tools) : toolRouteCatalog(availableGroups)}
|
|
102
102
|
|
|
103
103
|
Routing rules:
|
|
104
104
|
- You MUST call ${ROUTER_TOOL_NAME} exactly once and emit no prose.
|
|
@@ -125,7 +125,9 @@ Routing rules:
|
|
|
125
125
|
{ role: 'user', content: user },
|
|
126
126
|
];
|
|
127
127
|
try {
|
|
128
|
-
const result = await chat(messages, {}, request.signal, [
|
|
128
|
+
const result = await (request.transport ?? chat)(messages, {}, request.signal, [
|
|
129
|
+
routeSelectorTool(availableGroups),
|
|
130
|
+
]);
|
|
129
131
|
const call = result.toolCalls.find((toolCall) => toolCall.name === ROUTER_TOOL_NAME);
|
|
130
132
|
const parsed = call ? parseDecision(call.arguments, available, previousGroups, startedAt) : null;
|
|
131
133
|
if (parsed)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { beginPathMutation as defaultBeginPathMutation, beginWorkspaceMutation as defaultBeginWorkspaceMutation, endPathMutation as defaultEndPathMutation, endWorkspaceMutation as defaultEndWorkspaceMutation, getCurrentTurnMutationState as defaultGetCurrentTurnMutationState, } from '../rollback/index.js';
|
|
2
|
+
import { enforceSandbox as defaultEnforceSandbox } from '../sandbox/index.js';
|
|
3
|
+
import { t } from '../i18n/index.js';
|
|
4
|
+
import { resolveResourceLockRequests as defaultResolveResourceLockRequests, toolResourceLockManager, } from './resource-lock.js';
|
|
5
|
+
import { isToolErrorOutput } from './result.js';
|
|
6
|
+
import { validateToolArguments } from './validation.js';
|
|
7
|
+
const DEFAULT_CAPABILITIES = Object.freeze({
|
|
8
|
+
effect: 'unknown',
|
|
9
|
+
concurrency: 'serial',
|
|
10
|
+
});
|
|
11
|
+
const DEFAULT_DEPENDENCIES = {
|
|
12
|
+
enforceSandbox: defaultEnforceSandbox,
|
|
13
|
+
resolveResourceLockRequests: defaultResolveResourceLockRequests,
|
|
14
|
+
resourceLockManager: toolResourceLockManager,
|
|
15
|
+
beginPathMutation: defaultBeginPathMutation,
|
|
16
|
+
endPathMutation: defaultEndPathMutation,
|
|
17
|
+
beginWorkspaceMutation: defaultBeginWorkspaceMutation,
|
|
18
|
+
endWorkspaceMutation: defaultEndWorkspaceMutation,
|
|
19
|
+
getCurrentTurnMutationState: defaultGetCurrentTurnMutationState,
|
|
20
|
+
};
|
|
21
|
+
function buildToolIndex(list) {
|
|
22
|
+
return new Map(list.map((tool) => [tool.name, tool]));
|
|
23
|
+
}
|
|
24
|
+
function isStructuredOutcome(value) {
|
|
25
|
+
return (typeof value === 'object' &&
|
|
26
|
+
value !== null &&
|
|
27
|
+
typeof value.status === 'string' &&
|
|
28
|
+
typeof value.code === 'string' &&
|
|
29
|
+
typeof value.retryable === 'boolean' &&
|
|
30
|
+
typeof value.output === 'string');
|
|
31
|
+
}
|
|
32
|
+
function normalizeOutcome(value, durationMs, changedFiles) {
|
|
33
|
+
if (isStructuredOutcome(value)) {
|
|
34
|
+
return {
|
|
35
|
+
...value,
|
|
36
|
+
durationMs: value.durationMs ?? durationMs,
|
|
37
|
+
changedFiles: value.changedFiles ?? changedFiles,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const failed = isToolErrorOutput(value);
|
|
41
|
+
return {
|
|
42
|
+
status: failed ? 'error' : 'success',
|
|
43
|
+
code: failed ? 'EXECUTION_ERROR' : 'OK',
|
|
44
|
+
retryable: false,
|
|
45
|
+
output: value,
|
|
46
|
+
changedFiles,
|
|
47
|
+
durationMs,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function terminalOutcome(status, code, output, startedAt, changedFiles = []) {
|
|
51
|
+
return {
|
|
52
|
+
status,
|
|
53
|
+
code,
|
|
54
|
+
retryable: false,
|
|
55
|
+
output,
|
|
56
|
+
changedFiles,
|
|
57
|
+
durationMs: Date.now() - startedAt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function isTransientExecutionError(error) {
|
|
61
|
+
if (!error || typeof error !== 'object')
|
|
62
|
+
return false;
|
|
63
|
+
const value = error;
|
|
64
|
+
if (value.name === 'AbortError' || value.name === 'APIUserAbortError')
|
|
65
|
+
return false;
|
|
66
|
+
if (value.status === 408 || value.status === 429 || (typeof value.status === 'number' && value.status >= 500))
|
|
67
|
+
return true;
|
|
68
|
+
if (['ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'EPIPE'].includes(value.code ?? ''))
|
|
69
|
+
return true;
|
|
70
|
+
return (value.name === 'APIConnectionError' ||
|
|
71
|
+
value.name === 'APIConnectionTimeoutError' ||
|
|
72
|
+
(typeof value.message === 'string' && /\btime(?:d)?\s*out\b|ETIMEDOUT/i.test(value.message)));
|
|
73
|
+
}
|
|
74
|
+
function executionErrorOutcome(name, error, startedAt, changedFiles) {
|
|
75
|
+
const transient = isTransientExecutionError(error);
|
|
76
|
+
const value = error;
|
|
77
|
+
const timeout = transient &&
|
|
78
|
+
(value?.code === 'ETIMEDOUT' ||
|
|
79
|
+
value?.name === 'APIConnectionTimeoutError' ||
|
|
80
|
+
(error instanceof Error && /\btime(?:d)?\s*out\b|ETIMEDOUT/i.test(error.message)));
|
|
81
|
+
return {
|
|
82
|
+
status: 'error',
|
|
83
|
+
code: timeout ? 'TIMEOUT' : transient ? 'NETWORK_ERROR' : 'EXECUTION_ERROR',
|
|
84
|
+
retryable: transient,
|
|
85
|
+
output: t('toolError.execution', {
|
|
86
|
+
name,
|
|
87
|
+
message: error instanceof Error ? error.message : String(error),
|
|
88
|
+
}),
|
|
89
|
+
changedFiles,
|
|
90
|
+
durationMs: Date.now() - startedAt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Instance-scoped tool registry and executor. Builtins are installed explicitly to avoid a registry/builtins cycle. */
|
|
94
|
+
export class ToolRuntime {
|
|
95
|
+
/** Stable array identity: rebuilds always splice so existing consumers observe registration changes. */
|
|
96
|
+
tools = [];
|
|
97
|
+
extensions = new Map();
|
|
98
|
+
installedBuiltinTools = [];
|
|
99
|
+
toolIndex = buildToolIndex(this.tools);
|
|
100
|
+
dependencies;
|
|
101
|
+
constructor(dependencies = {}) {
|
|
102
|
+
this.dependencies = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
103
|
+
}
|
|
104
|
+
/** The currently installed builtin list, retained by reference for compatibility with the global registry. */
|
|
105
|
+
get builtinTools() {
|
|
106
|
+
return this.installedBuiltinTools;
|
|
107
|
+
}
|
|
108
|
+
installBuiltinTools(list) {
|
|
109
|
+
this.installedBuiltinTools = list;
|
|
110
|
+
this.rebuildTools();
|
|
111
|
+
}
|
|
112
|
+
registerToolsExtension(sourceOrAdditions, maybeAdditions) {
|
|
113
|
+
const source = typeof sourceOrAdditions === 'string' ? sourceOrAdditions : 'external';
|
|
114
|
+
const additions = typeof sourceOrAdditions === 'string' ? (maybeAdditions ?? []) : sourceOrAdditions;
|
|
115
|
+
this.extensions.set(source, additions);
|
|
116
|
+
const rejected = additions
|
|
117
|
+
.filter((tool) => this.installedBuiltinTools.some((builtin) => builtin.name === tool.name))
|
|
118
|
+
.map((tool) => tool.name);
|
|
119
|
+
this.rebuildTools();
|
|
120
|
+
return rejected;
|
|
121
|
+
}
|
|
122
|
+
clearToolsExtension(source) {
|
|
123
|
+
if (this.extensions.delete(source))
|
|
124
|
+
this.rebuildTools();
|
|
125
|
+
}
|
|
126
|
+
findTool(name) {
|
|
127
|
+
return this.toolIndex.get(name);
|
|
128
|
+
}
|
|
129
|
+
/** 缺少声明或找不到工具时返回保守能力,绝不把未知扩展并发执行。 */
|
|
130
|
+
getToolCapabilities(toolOrName) {
|
|
131
|
+
const tool = typeof toolOrName === 'string' ? this.findTool(toolOrName) : toolOrName;
|
|
132
|
+
return tool?.capabilities ?? DEFAULT_CAPABILITIES;
|
|
133
|
+
}
|
|
134
|
+
getToolResourceKeys(toolOrName, args) {
|
|
135
|
+
const capabilities = this.getToolCapabilities(toolOrName);
|
|
136
|
+
try {
|
|
137
|
+
return capabilities.resources?.(args) ?? [];
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** resource-locked write 是可生成文件 diff/按路径记 rollback 的文件 mutation。 */
|
|
144
|
+
isFileMutationCapabilities(capabilities) {
|
|
145
|
+
return capabilities.effect === 'write' && capabilities.concurrency === 'resource-locked';
|
|
146
|
+
}
|
|
147
|
+
/** 按工具名判定(兼容入口);热路径请直接复用已解析的 capabilities。 */
|
|
148
|
+
isFileMutationTool(name) {
|
|
149
|
+
return this.isFileMutationCapabilities(this.getToolCapabilities(name));
|
|
150
|
+
}
|
|
151
|
+
/** 结构化工具调度入口。永不抛错;旧字符串工具在此归一化为 ToolOutcome。 */
|
|
152
|
+
async executeToolOutcome(name, argsRaw, signal, opts) {
|
|
153
|
+
const startedAt = Date.now();
|
|
154
|
+
if (signal?.aborted) {
|
|
155
|
+
return terminalOutcome('aborted', 'ABORTED', t('command.interrupted'), startedAt);
|
|
156
|
+
}
|
|
157
|
+
const tool = this.findTool(name);
|
|
158
|
+
if (!tool) {
|
|
159
|
+
return terminalOutcome('error', 'UNKNOWN_TOOL', t('toolError.unknown', { name }), startedAt);
|
|
160
|
+
}
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = argsRaw.trim() ? JSON.parse(argsRaw) : {};
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return terminalOutcome('error', 'INVALID_JSON', t('toolError.invalidJson', { name, arguments: argsRaw }), startedAt);
|
|
167
|
+
}
|
|
168
|
+
const validation = validateToolArguments(tool, parsed);
|
|
169
|
+
if (!validation.valid) {
|
|
170
|
+
const hint = opts?.argumentErrorHint?.trim();
|
|
171
|
+
const message = hint
|
|
172
|
+
? `错误:工具 ${name} 参数无效: ${validation.message}\n${hint}`
|
|
173
|
+
: `错误:工具 ${name} 参数无效: ${validation.message}`;
|
|
174
|
+
return terminalOutcome('error', validation.code, message, startedAt);
|
|
175
|
+
}
|
|
176
|
+
const args = parsed;
|
|
177
|
+
try {
|
|
178
|
+
const sandboxError = this.dependencies.enforceSandbox(name, args);
|
|
179
|
+
if (sandboxError) {
|
|
180
|
+
return terminalOutcome('denied', 'SANDBOX_DENIED', sandboxError, startedAt);
|
|
181
|
+
}
|
|
182
|
+
return await this.executeToolOnce(tool, args, signal, opts);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return executionErrorOutcome(name, error, startedAt, []);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** 字符串兼容入口:现有调用方、TUI 和 LLM history 无需同步迁移。 */
|
|
189
|
+
async executeTool(name, argsRaw, signal, opts) {
|
|
190
|
+
return (await this.executeToolOutcome(name, argsRaw, signal, opts)).output;
|
|
191
|
+
}
|
|
192
|
+
rebuildTools() {
|
|
193
|
+
const names = new Set();
|
|
194
|
+
const next = [];
|
|
195
|
+
for (const tool of [...this.installedBuiltinTools, ...Array.from(this.extensions.values()).flat()]) {
|
|
196
|
+
if (names.has(tool.name))
|
|
197
|
+
continue;
|
|
198
|
+
names.add(tool.name);
|
|
199
|
+
next.push(tool);
|
|
200
|
+
}
|
|
201
|
+
this.tools.splice(0, this.tools.length, ...next);
|
|
202
|
+
this.toolIndex = buildToolIndex(next);
|
|
203
|
+
}
|
|
204
|
+
/** Execute one tool call while holding its declared locks and capturing rollback state. */
|
|
205
|
+
async executeToolOnce(tool, args, signal, opts) {
|
|
206
|
+
const startedAt = Date.now();
|
|
207
|
+
const capabilities = this.getToolCapabilities(tool);
|
|
208
|
+
let mutationVersionBefore;
|
|
209
|
+
let capturedPath;
|
|
210
|
+
try {
|
|
211
|
+
const requests = this.dependencies.resolveResourceLockRequests(capabilities, args);
|
|
212
|
+
return await this.dependencies.resourceLockManager.withLocks(requests, signal, async () => {
|
|
213
|
+
opts?.onLockAcquired?.(args);
|
|
214
|
+
const mutationBefore = this.dependencies.getCurrentTurnMutationState();
|
|
215
|
+
mutationVersionBefore = mutationBefore.version;
|
|
216
|
+
const pathCapture = !capabilities.delegatesResourceLocks &&
|
|
217
|
+
this.isFileMutationCapabilities(capabilities) &&
|
|
218
|
+
typeof args.path === 'string' &&
|
|
219
|
+
args.path
|
|
220
|
+
? this.dependencies.beginPathMutation(args.path)
|
|
221
|
+
: null;
|
|
222
|
+
capturedPath = pathCapture?.path;
|
|
223
|
+
const workspaceCapture = capabilities.effect === 'process' || capabilities.effect === 'unknown'
|
|
224
|
+
? await this.dependencies.beginWorkspaceMutation()
|
|
225
|
+
: null;
|
|
226
|
+
let raw;
|
|
227
|
+
try {
|
|
228
|
+
raw = await tool.execute(args, {
|
|
229
|
+
signal,
|
|
230
|
+
callId: opts?.callId,
|
|
231
|
+
allowedToolNames: opts?.allowedToolNames,
|
|
232
|
+
delegation: opts?.delegation,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
if (pathCapture)
|
|
237
|
+
this.dependencies.endPathMutation(pathCapture, tool.name);
|
|
238
|
+
if (workspaceCapture)
|
|
239
|
+
await this.dependencies.endWorkspaceMutation(workspaceCapture, tool.name);
|
|
240
|
+
}
|
|
241
|
+
const mutationAfter = this.dependencies.getCurrentTurnMutationState();
|
|
242
|
+
const changedFiles = mutationAfter.version !== mutationBefore.version
|
|
243
|
+
? pathCapture
|
|
244
|
+
? mutationAfter.changedFiles.filter((item) => item.path === pathCapture.path).map((item) => item.path)
|
|
245
|
+
: mutationAfter.changedFiles.map((item) => item.path)
|
|
246
|
+
: [];
|
|
247
|
+
if (signal?.aborted) {
|
|
248
|
+
const aborted = terminalOutcome('aborted', 'ABORTED', String(isStructuredOutcome(raw) ? raw.output : raw), startedAt, changedFiles);
|
|
249
|
+
return isStructuredOutcome(raw) ? { ...aborted, usage: raw.usage } : aborted;
|
|
250
|
+
}
|
|
251
|
+
return normalizeOutcome(raw, Date.now() - startedAt, changedFiles);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
const mutationAfter = this.dependencies.getCurrentTurnMutationState();
|
|
256
|
+
const changedFiles = mutationVersionBefore !== undefined && mutationAfter.version !== mutationVersionBefore
|
|
257
|
+
? capturedPath
|
|
258
|
+
? mutationAfter.changedFiles.filter((item) => item.path === capturedPath).map((item) => item.path)
|
|
259
|
+
: mutationAfter.changedFiles.map((item) => item.path)
|
|
260
|
+
: [];
|
|
261
|
+
if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {
|
|
262
|
+
return terminalOutcome('aborted', 'ABORTED', t('command.interrupted'), startedAt, changedFiles);
|
|
263
|
+
}
|
|
264
|
+
return executionErrorOutcome(tool.name, error, startedAt, changedFiles);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -492,6 +492,10 @@ export function isLastContentRowBlank() {
|
|
|
492
492
|
export function normalizeMutationBoundary() {
|
|
493
493
|
if (!state.active || !ui.isTTY)
|
|
494
494
|
return;
|
|
495
|
+
// 空缓冲(turn 首工具且此前无任何内容)不归一:normalizeTrailingBlankRows 会凭空
|
|
496
|
+
// push 一条空行,导致屏顶多一条空白(首条摘要前多一空行)。
|
|
497
|
+
if (content.committedRows() === 0 && content.currentRowRaw() === null)
|
|
498
|
+
return;
|
|
495
499
|
if (state.mdActive)
|
|
496
500
|
commitMd();
|
|
497
501
|
const totalBefore = content.totalRows();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,9 +20,12 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "tsx src/index.ts",
|
|
23
|
-
"build": "tsc -p tsconfig.build.json",
|
|
23
|
+
"build": "npm run build --workspace @mocode/protocol && npm run build --workspace @mocode/runtime && tsc -p tsconfig.build.json",
|
|
24
|
+
"build:electron": "npm run build && npm run check:electron-boundary && npm run typecheck --workspace mocode-work && npm run build --workspace mocode-work && npm run typecheck --workspace mocode-pet-app && npm run build --workspace mocode-pet-app && npm run smoke:runtime-host",
|
|
24
25
|
"test": "tsc -p tsconfig.test-build.json && node --test --experimental-test-isolation=none \"dist-tests/tests/*.test.js\"",
|
|
25
|
-
"typecheck": "tsc --noEmit && tsc -p tests/tsconfig.json && tsc -p evals/tsconfig.json",
|
|
26
|
+
"typecheck": "npm run typecheck --workspace @mocode/protocol && npm run typecheck --workspace @mocode/runtime && tsc --noEmit && tsc -p tests/tsconfig.json && tsc -p evals/tsconfig.json",
|
|
27
|
+
"check:electron-boundary": "npm run check:electron-boundary --workspace @mocode/runtime",
|
|
28
|
+
"smoke:runtime-host": "npm run smoke:host --workspace @mocode/runtime",
|
|
26
29
|
"lint": "eslint .",
|
|
27
30
|
"lint:check": "eslint .",
|
|
28
31
|
"lint:fix": "eslint . --fix",
|
|
@@ -43,6 +46,7 @@
|
|
|
43
46
|
"ws": "8.21.0"
|
|
44
47
|
},
|
|
45
48
|
"devDependencies": {
|
|
49
|
+
"@mocode/protocol": "0.0.0",
|
|
46
50
|
"@types/node": "^22.0.0",
|
|
47
51
|
"@types/ws": "8.5.13",
|
|
48
52
|
"eslint": "^9.39.5",
|