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.
Files changed (47) hide show
  1. package/README.md +13 -1
  2. package/dist/agent/core.js +14 -936
  3. package/dist/agent/index.js +37 -13
  4. package/dist/agent/model-turn.js +218 -0
  5. package/dist/agent/pipeline.js +18 -0
  6. package/dist/agent/run-contracts.js +1 -0
  7. package/dist/agent/run-coordinator.js +758 -0
  8. package/dist/agent/runtime-context.js +118 -24
  9. package/dist/agent/spawn.js +11 -7
  10. package/dist/agent/stages/context-trimmer.js +63 -0
  11. package/dist/agent/stages/contracts.js +12 -0
  12. package/dist/agent/stages/history-manager.js +178 -0
  13. package/dist/agent/stages/legacy-adapters.js +19 -0
  14. package/dist/agent/stages/model-runner.js +29 -0
  15. package/dist/agent/stages/run-policy.js +73 -0
  16. package/dist/agent/stages/tool-dispatcher.js +341 -0
  17. package/dist/agent/tool-helpers.js +12 -12
  18. package/dist/agent/tool-turn.js +87 -0
  19. package/dist/agent/trace-state.js +97 -101
  20. package/dist/agent/turn-lifecycle.js +110 -0
  21. package/dist/config/index.js +14 -0
  22. package/dist/host/stdio.js +101 -40
  23. package/dist/llm/index.js +51 -35
  24. package/dist/llm/providers/anthropic.js +16 -10
  25. package/dist/llm/runtime.js +1 -0
  26. package/dist/permissions/index.js +21 -5
  27. package/dist/repl/commands/compact.js +2 -2
  28. package/dist/repl/commands/session.js +3 -12
  29. package/dist/repl/message-format.js +5 -0
  30. package/dist/repl/runtime.js +95 -55
  31. package/dist/rollback/index.js +29 -624
  32. package/dist/rollback/store.js +593 -0
  33. package/dist/runtime/index.js +1 -0
  34. package/dist/runtime/runtime.js +307 -0
  35. package/dist/session/compact.js +22 -14
  36. package/dist/session/index.js +1 -0
  37. package/dist/session/persist.js +10 -146
  38. package/dist/session/scheduler.js +28 -16
  39. package/dist/session/state.js +16 -12
  40. package/dist/session/store.js +218 -0
  41. package/dist/session/trace.js +5 -15
  42. package/dist/tools/policy.js +19 -15
  43. package/dist/tools/registry.js +21 -229
  44. package/dist/tools/router.js +5 -3
  45. package/dist/tools/tool-runtime.js +267 -0
  46. package/dist/ui/layout-internal/content-write.js +4 -0
  47. package/package.json +7 -3
@@ -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
- * 可扩展工具注册表。数组实例始终稳定,使已经持有 tools 引用的 agent/LLM 能看到运行时新增工具。
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 + 已注册扩展。
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
- builtinTools = list;
31
- rebuildTools();
13
+ defaultToolRuntime.installBuiltinTools(list);
32
14
  }
33
15
  export function registerToolsExtension(sourceOrAdditions, maybeAdditions) {
34
- const source = typeof sourceOrAdditions === 'string' ? sourceOrAdditions : 'external';
35
- const additions = typeof sourceOrAdditions === 'string' ? (maybeAdditions ?? []) : sourceOrAdditions;
36
- extensions.set(source, additions);
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
- if (extensions.delete(source))
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 toolIndex.get(name);
24
+ return defaultToolRuntime.findTool(name);
65
25
  }
66
26
  /** 缺少声明或找不到工具时返回保守能力,绝不把未知扩展并发执行。 */
67
27
  export function getToolCapabilities(toolOrName) {
68
- const tool = typeof toolOrName === 'string' ? findTool(toolOrName) : toolOrName;
69
- return tool?.capabilities ?? DEFAULT_CAPABILITIES;
28
+ return defaultToolRuntime.getToolCapabilities(toolOrName);
70
29
  }
71
30
  export function getToolResourceKeys(toolOrName, args) {
72
- const capabilities = getToolCapabilities(toolOrName);
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 capabilities.effect === 'write' && capabilities.concurrency === 'resource-locked';
35
+ return defaultToolRuntime.isFileMutationCapabilities(capabilities);
83
36
  }
84
- /** 按工具名判定(兼容入口);热路径请直接复用已解析的 capabilities 走 isFileMutationCapabilities。 */
37
+ /** 按工具名判定(兼容入口);热路径请直接复用已解析的 capabilities。 */
85
38
  export function isFileMutationTool(name) {
86
- return isFileMutationCapabilities(getToolCapabilities(name));
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
- const startedAt = Date.now();
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 (await executeToolOutcome(name, argsRaw, signal, opts)).output;
50
+ return defaultToolRuntime.executeTool(name, argsRaw, signal, opts);
260
51
  }
52
+ export { ToolRuntime } from './tool-runtime.js';
@@ -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, [routeSelectorTool(availableGroups)]);
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.2",
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",