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
package/dist/agent/index.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
// 行为与重构前的单文件 runAgent 完全一致——这是安全重构,实跑回归验证。
|
|
5
5
|
import { ui } from '../ui/theme.js';
|
|
6
6
|
import { Spinner } from '../ui/spinner.js';
|
|
7
|
-
import { summarizeToolCall, summarizeToolResult, truncateDisplay, fmtElapsed
|
|
7
|
+
import { summarizeToolCall, summarizeToolResult, truncateDisplay, fmtElapsed } from '../ui/render.js';
|
|
8
8
|
import { renderFileChange } from '../ui/diff.js';
|
|
9
9
|
import * as layout from '../ui/layout.js';
|
|
10
10
|
import * as batch from '../ui/batch.js';
|
|
11
11
|
import { beginTurn } from '../rollback/index.js';
|
|
12
12
|
import { config } from '../config/index.js';
|
|
13
|
-
import { runAgentCore, isMutationTool
|
|
13
|
+
import { runAgentCore, isMutationTool } from './core.js';
|
|
14
14
|
import { createPetHooks } from '../pet/state.js';
|
|
15
15
|
import { t } from '../i18n/index.js';
|
|
16
16
|
import { isToolErrorOutput } from '../tools/result.js';
|
|
@@ -39,9 +39,9 @@ function lineDelta(oldText, newText) {
|
|
|
39
39
|
while (head < before.length && head < after.length && before[head] === after[head])
|
|
40
40
|
head++;
|
|
41
41
|
let tail = 0;
|
|
42
|
-
while (tail < before.length - head
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
while (tail < before.length - head &&
|
|
43
|
+
tail < after.length - head &&
|
|
44
|
+
before[before.length - 1 - tail] === after[after.length - 1 - tail])
|
|
45
45
|
tail++;
|
|
46
46
|
return { added: after.length - head - tail, removed: before.length - head - tail };
|
|
47
47
|
}
|
|
@@ -127,7 +127,7 @@ function writeToolHeader(tc) {
|
|
|
127
127
|
}
|
|
128
128
|
else {
|
|
129
129
|
// 普通工具合并到 currentBatchId;同轮并行或连续无正文的工具轮次共享一个摘要行。
|
|
130
|
-
const id = currentBatchId ??= batch.beginBatch();
|
|
130
|
+
const id = (currentBatchId ??= batch.beginBatch());
|
|
131
131
|
// 结果按 tool_call id 归位:并行执行时 currentBatchId 会漂移,只按“当前批”回填会漏填,
|
|
132
132
|
// 摘要行就永远停在 ◇(用户实测:子 agent 跑完主侧菱形没变成实心圆)。
|
|
133
133
|
batch.bindCall(tc.id, id);
|
|
@@ -166,9 +166,7 @@ function writeToolResult(tc, output, parsed, preWriteOld, editStartLine) {
|
|
|
166
166
|
diff = renderFileChange({
|
|
167
167
|
path: String(parsed.path ?? ''),
|
|
168
168
|
kind: tc.name === 'edit_file' ? 'edit' : 'write',
|
|
169
|
-
oldStr: tc.name === 'edit_file'
|
|
170
|
-
? String(parsed.old_string ?? '')
|
|
171
|
-
: preWriteOld,
|
|
169
|
+
oldStr: tc.name === 'edit_file' ? String(parsed.old_string ?? '') : preWriteOld,
|
|
172
170
|
newStr: String((tc.name === 'edit_file' ? parsed.new_string : parsed.content) ?? ''),
|
|
173
171
|
startLine: tc.name === 'edit_file' ? editStartLine : 1,
|
|
174
172
|
});
|
|
@@ -248,7 +246,11 @@ function flushToolBatch(expandSingleEntry = false) {
|
|
|
248
246
|
*/
|
|
249
247
|
export async function runAgent(history, userInput, signal,
|
|
250
248
|
/** 每步 chat() 返回后回调:repl 据此重算并重画状态行 context 用量条(运行中实时刷新,不冻结在轮首)。 */
|
|
251
|
-
onContextUpdate
|
|
249
|
+
onContextUpdate,
|
|
250
|
+
/** 本用户 turn 的自动路由工具策略。 */
|
|
251
|
+
toolPolicy,
|
|
252
|
+
/** 仅真实用户 turn 传入;合成 plan 执行轮继承 policy 但不重复记录初始路由。 */
|
|
253
|
+
initialToolRoute) {
|
|
252
254
|
// 开新轮次(回滚用):首行截断 40,供 /rollback 轮次菜单展示。
|
|
253
255
|
beginTurn(truncateDisplay(firstLineOf(userInput), 40));
|
|
254
256
|
layout.contentMode(); // 防御性:运行态光标归输入框光标位供 IME 锚定(enterRunningMode 已置,这里兜底)
|
|
@@ -423,6 +425,8 @@ onContextUpdate) {
|
|
|
423
425
|
signal,
|
|
424
426
|
onContextUpdate,
|
|
425
427
|
hooks: combinedHooks,
|
|
428
|
+
toolPolicy,
|
|
429
|
+
initialToolRoute,
|
|
426
430
|
onTraceEvent: appendCurrentSessionTraceEvent,
|
|
427
431
|
});
|
|
428
432
|
}
|
package/dist/agent/mode.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 把 `agentMode` 从 repl 的模块变量提到这里,让三方都能用、且不破坏依赖单向:
|
|
5
5
|
* - `repl/index.ts` 读写它 + 注册 onModeChange 监听器(做 applyMode 重写 history[0] +
|
|
6
6
|
* refreshStatusBase 刷状态行 modeTag),并通过 /plan / /auto / Shift+Tab(c cycleMode) 触发切换。
|
|
7
|
-
* - `agent/index.ts`
|
|
8
|
-
*
|
|
7
|
+
* - `agent/index.ts` 每步读取模式:PLAN 只暴露只读能力;AUTO 允许执行,但实际工具面由
|
|
8
|
+
* 每个真实用户 turn 的 ToolPolicy 路由并可在后续 step 单向扩容。
|
|
9
9
|
* - 模型不再持有 switch_mode 工具(已砍):模式切换只能由用户面触发。
|
|
10
10
|
*
|
|
11
11
|
* 依赖方向无环:本模块不 import 任何业务模块。
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
let currentMode = 'auto';
|
|
19
19
|
let listener = null;
|
|
20
|
-
/** 当前 agent 模式。auto
|
|
20
|
+
/** 当前 agent 模式。auto=按任务路由能力并执行;plan=只读探查 + 产出计划。 */
|
|
21
21
|
export function getAgentMode() {
|
|
22
22
|
return currentMode;
|
|
23
23
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentRuntimeContext:runAgentCore 的运行时依赖注入接缝。
|
|
3
|
+
*
|
|
4
|
+
* 2.0 框架化第一步(Context 参数化):把 core.ts 直读的模块级单例(config / agentMode /
|
|
5
|
+
* sessionId / sandboxRoot / token 校准 / notes mtime)收敛成一个显式接口。默认实现
|
|
6
|
+
* defaultAgentRuntimeContext 原样绑定全局单例,行为与改造前完全一致;宿主(TUI / stdio host /
|
|
7
|
+
* 子 agent / 未来多 runtime)可注入自定义实现,在同一进程内获得互不干扰的运行时视图。
|
|
8
|
+
*
|
|
9
|
+
* 本模块是叶子:只依赖各单例所在模块,不反向 import core.ts,依赖方向无环。
|
|
10
|
+
*
|
|
11
|
+
* 取舍说明:
|
|
12
|
+
* - 用方法(getter)而非裸值字段:config.model 等会被 /model 热切,方法保证每次读取取最新值,
|
|
13
|
+
* 与改造前「每次直读全局」的语义一致。
|
|
14
|
+
* - jailResolve / getCurrentTurnMutationState 等涉及真实 I/O 或可变返回,保持函数形态原样透传。
|
|
15
|
+
* - 暂未覆盖 executeToolOutcome / findTool(tools registry 单例)与 checkPermission:它们属
|
|
16
|
+
* 工具系统(步骤 4 抽包时处理),本轮只收敛 agent 运行时自身的单例。
|
|
17
|
+
*/
|
|
18
|
+
import { config, getActiveModel, extractActivePlanSection, buildSessionStateReminder } from '../config/index.js';
|
|
19
|
+
import { getAgentMode, setAgentMode } from './mode.js';
|
|
20
|
+
import { getCurrentSessionId } from '../session/state.js';
|
|
21
|
+
import { getCurrentTurnId, getCurrentTurnMutationState } from '../rollback/index.js';
|
|
22
|
+
import { getNotesMtime } from '../session/notes.js';
|
|
23
|
+
import { jailResolve } from '../sandbox/index.js';
|
|
24
|
+
import { getTokenCalibration, updateTokenCalibration } from '../context/token-calibration.js';
|
|
25
|
+
import { safeProviderId } from '../session/trace-sanitize.js';
|
|
26
|
+
/**
|
|
27
|
+
* 默认运行时上下文:原样绑定全局单例,与改造前 runAgentCore 的直读行为完全一致。
|
|
28
|
+
* 每个方法都是对应单例函数的透传;config 是同一对象引用(字段读取热生效)。
|
|
29
|
+
*/
|
|
30
|
+
export const defaultAgentRuntimeContext = {
|
|
31
|
+
config,
|
|
32
|
+
getActiveModel,
|
|
33
|
+
getAgentMode,
|
|
34
|
+
setAgentMode,
|
|
35
|
+
getCurrentSessionId,
|
|
36
|
+
getCurrentTurnId,
|
|
37
|
+
getCurrentTurnMutationState,
|
|
38
|
+
buildSessionStateReminder,
|
|
39
|
+
extractActivePlanSection,
|
|
40
|
+
getNotesMtime,
|
|
41
|
+
jailResolve,
|
|
42
|
+
getTokenCalibration,
|
|
43
|
+
updateTokenCalibration,
|
|
44
|
+
safeProviderId,
|
|
45
|
+
};
|
package/dist/agent/spawn.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
// 子 agent 封装:runAgentCore + (TUI 激活时)实时渲染 hooks
|
|
1
|
+
// 子 agent 封装:runAgentCore + (TUI 激活时)实时渲染 hooks。
|
|
2
2
|
// 供 task 工具(主 agent 派生子任务)调用——子 agent 的最终摘要回灌主 history。
|
|
3
3
|
//
|
|
4
|
-
// 与主 agent
|
|
5
|
-
// -
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// -
|
|
9
|
-
//
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
12
|
-
// -
|
|
13
|
-
// - 中断透传:opts.signal(主 agent 的 abort signal)透传给 runAgentCore → chat/executeTool,
|
|
14
|
-
// 主 Ctrl+C 树杀子 agent(chat 流式 abort + run_command/web_fetch 即时取消)。
|
|
4
|
+
// 子 agent 与主 agent 完全同源(无任何能力裁剪):
|
|
5
|
+
// - 同一份系统提示、同一份工具 schema、同一份对话前缀(delegation 命中前缀缓存);
|
|
6
|
+
// - 写操作直接落在工作区,进入主 agent 当前轮次的同一回滚事务(spawn 不调 beginTurn);
|
|
7
|
+
// - 步数默认与主 agent 相同,只作为无限循环保险,不以 token 配额提前终止有效任务;
|
|
8
|
+
// - 中断透传:opts.signal 透传给 runAgentCore → chat/executeTool,主 Ctrl+C 树杀子 agent。
|
|
9
|
+
// 与主 agent 的唯一差别是渲染与 history 归属:
|
|
10
|
+
// - 主屏渲染可选:TUI 激活时把子 agent 内部工具调用实时写入主内容区并复用 batch 折叠;
|
|
11
|
+
// TUI 未激活(host 嵌入 / 非 TTY)时纯静默,中间过程只缓冲进 transcript。
|
|
12
|
+
// - 独立 history 分支:子任务的工具噪声不回灌主对话,只有最终摘要回灌。
|
|
15
13
|
import { chatTools } from '../llm/index.js';
|
|
16
|
-
import { buildMocodeCorePrompt, config,
|
|
14
|
+
import { buildMocodeCorePrompt, config, isSubAgentHardDisabled } from '../config/index.js';
|
|
15
|
+
import { getToolChatSchema } from '../tools/policy.js';
|
|
17
16
|
import { effectiveSystemPrompt } from '../skills/index.js';
|
|
18
17
|
import { ui } from '../ui/theme.js';
|
|
19
18
|
import * as layout from '../ui/layout.js';
|
|
@@ -24,21 +23,18 @@ import { runAgentCore } from './core.js';
|
|
|
24
23
|
import { summarizeToolCall, summarizeToolResult, truncateDisplay } from '../ui/render.js';
|
|
25
24
|
import { t } from '../i18n/index.js';
|
|
26
25
|
import { createContextState } from '../session/compact.js';
|
|
27
|
-
|
|
28
|
-
/** 子 agent 系统提示后缀:角色与约束。 */
|
|
26
|
+
/** 子 agent 系统提示后缀(仅 legacy 直接调用路径用;共享前缀路径的系统提示直接复用父 agent)。 */
|
|
29
27
|
const SUBAGENT_SUFFIX = `
|
|
30
28
|
|
|
31
29
|
## ⛯ SUB-AGENT MODE (you are a sub-agent)
|
|
32
|
-
You are a sub-agent spawned by the main agent to handle
|
|
33
|
-
- Focus
|
|
34
|
-
-
|
|
35
|
-
- When done, your final text reply will be returned to the main agent as a summary — make it concise and actionable: what you did, key findings, files changed, and any issues. The main agent will decide the next step based on your summary.`;
|
|
30
|
+
You are a sub-agent spawned by the main agent to handle a delegated sub-task, with the same tools and full capabilities as the main agent.
|
|
31
|
+
- Focus on the assigned sub-task and see it through to completion.
|
|
32
|
+
- When done, your final text reply is returned to the main agent as the result — concise and actionable: what you did, key findings, files changed, blockers.`;
|
|
36
33
|
const SUBAGENT_ROLE = `## Sub-agent execution
|
|
37
|
-
You are executing one delegated sub-task with the same engineering standards and capabilities as
|
|
34
|
+
You are executing one delegated sub-task with the same engineering standards and full capabilities as the main agent.
|
|
38
35
|
- Treat Task context as authoritative facts already established by the main agent; do not rediscover them without evidence they are stale.
|
|
39
36
|
- Focus on the delegated scope, but continue until it is genuinely complete. Do not stop to save tokens.
|
|
40
|
-
-
|
|
41
|
-
- Return concise findings, changes, checks you chose to run, and blockers to the coordinator.`;
|
|
37
|
+
- Return concise findings, changes, checks you chose to run, and blockers to the main agent.`;
|
|
42
38
|
/**
|
|
43
39
|
* 派生一个子 agent 执行独立子任务。
|
|
44
40
|
*
|
|
@@ -52,39 +48,80 @@ You are executing one delegated sub-task with the same engineering standards and
|
|
|
52
48
|
* 子 agent 跑在主 signal 下,主 abort 即子 abort;子 agent 的 abortRestore 还原子 history + 模式。
|
|
53
49
|
*/
|
|
54
50
|
export async function spawnAgent(opts) {
|
|
55
|
-
if (
|
|
51
|
+
if (isSubAgentHardDisabled()) {
|
|
56
52
|
return {
|
|
57
53
|
summary: null,
|
|
58
54
|
completed: false,
|
|
59
|
-
transcript: 'Sub-agent execution is disabled
|
|
60
|
-
status: 'failed',
|
|
55
|
+
transcript: 'Sub-agent execution is disabled by MOCODE_SUBAGENT_ENABLED=false.',
|
|
56
|
+
status: 'failed',
|
|
61
57
|
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, reasoningTokens: 0 },
|
|
62
58
|
};
|
|
63
59
|
}
|
|
64
60
|
const maxSteps = opts.maxSteps ?? config.subAgentMaxSteps;
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
? `Task context (authoritative; do not rediscover):\n${opts.context.slice(0, 4000)}\n\nSub-task:\n${opts.prompt}`
|
|
70
|
-
: opts.prompt;
|
|
71
|
-
// 写 worker 保留主 Agent 的完整能力;只读 mode 仅按调用契约移除副作用工具。
|
|
61
|
+
const requested = opts.tools === undefined ? null : new Set(opts.tools);
|
|
62
|
+
const shared = opts.delegation && opts.delegation.history.length > 0 && opts.delegation.history[0]?.role === 'system'
|
|
63
|
+
? opts.delegation
|
|
64
|
+
: null;
|
|
72
65
|
let toolsOverride;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
66
|
+
let runtimeAllowedToolNames;
|
|
67
|
+
let history;
|
|
68
|
+
let userInput;
|
|
69
|
+
if (shared) {
|
|
70
|
+
// ── 共享前缀模式(官方 TUI/stdio 派发路径)──
|
|
71
|
+
// 系统提示 = 父 history[0](含 skills 段),工具 schema = 父 step activeTools,
|
|
72
|
+
// history = 父前缀(上游已去掉尾部「产生本次调用的 assistant tool_call 消息」)。
|
|
73
|
+
// 整条前缀与主 agent 已发送内容逐字节一致 → 命中前缀缓存;子 agent 直接看到
|
|
74
|
+
// 主 agent 已读过的文件与结论,不再重复探索。
|
|
75
|
+
// 不做任何裁剪:子 agent 的工具面就是父 step 的工具面。
|
|
76
|
+
let tools = [...shared.tools];
|
|
77
|
+
if (requested) {
|
|
78
|
+
// 显式 tools 白名单是调用方主动的特化收缩(skill allowed-tools),可裁 schema。
|
|
79
|
+
tools = tools.filter((tool) => requested.has(tool.function.name));
|
|
80
|
+
}
|
|
81
|
+
toolsOverride = tools;
|
|
82
|
+
// schema 即上限。显式给 runtimeAllowedToolNames 可让 core 跳过 legacy profile 防线,
|
|
83
|
+
// 否则旧 profile 会误删父 policy 已授予的工具(如 memory-*)——子代理必须与父同源。
|
|
84
|
+
runtimeAllowedToolNames = new Set(tools.map((tool) => tool.function.name));
|
|
85
|
+
history = [...shared.history];
|
|
86
|
+
userInput = [
|
|
87
|
+
'[Sub-agent delegation] The main agent delegated a sub-task to you. The conversation above is the shared working context — treat facts already established there as ground truth; do NOT redo exploration that is already done.',
|
|
88
|
+
opts.context?.trim() ? `Task context (authoritative; do not rediscover):\n${opts.context.slice(0, 4000)}` : '',
|
|
89
|
+
`Sub-task:\n${opts.prompt}`,
|
|
90
|
+
[
|
|
91
|
+
"You have the same tools and the same full capabilities as the main agent. Your file writes land directly in the workspace and belong to the main agent's current rollback transaction.",
|
|
92
|
+
'Your final text reply is returned to the main agent as the result — concise and actionable: what you did, key findings, files changed, blockers.',
|
|
93
|
+
].join('\n'),
|
|
94
|
+
opts.systemPromptSuffix ?? '',
|
|
95
|
+
]
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join('\n\n');
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// ── legacy 紧凑上下文(直接调用 / 未迁移嵌入 / 测试)──
|
|
101
|
+
// 没有父前缀可复用时自建 worker 上下文;能力面仍以父 step 上限为准,不额外收缩。
|
|
102
|
+
const systemPrompt = effectiveSystemPrompt(buildMocodeCorePrompt() +
|
|
103
|
+
'\n\n' +
|
|
104
|
+
SUBAGENT_ROLE +
|
|
105
|
+
SUBAGENT_SUFFIX +
|
|
106
|
+
(opts.systemPromptSuffix ? `\n\n${opts.systemPromptSuffix}` : ''));
|
|
107
|
+
// 子 worker 只能在父 step 的不可变 capability 上限内进一步缩小。没有父快照时以当前
|
|
108
|
+
// chatTools 为 baseline;显式 tools:[] 必须保持零工具,不能误当成"未限制"。
|
|
109
|
+
const parentNames = opts.parentAllowedToolNames
|
|
110
|
+
? [...new Set(opts.parentAllowedToolNames)]
|
|
111
|
+
: chatTools.map((tool) => tool.function.name);
|
|
112
|
+
const effectiveNames = parentNames.filter((name) => !requested || requested.has(name));
|
|
113
|
+
toolsOverride = effectiveNames.flatMap((name) => {
|
|
114
|
+
const schema = getToolChatSchema(name);
|
|
115
|
+
return schema ? [schema] : [];
|
|
116
|
+
});
|
|
117
|
+
runtimeAllowedToolNames = new Set(toolsOverride.map((tool) => tool.function.name));
|
|
118
|
+
// 独立 history(子 agent 自己持有,不共享主对话)。
|
|
119
|
+
// 只塞 system;user 消息由 runAgentCore 的 userInput 参数 push(与主 agent 一致)。
|
|
120
|
+
history = [{ role: 'system', content: systemPrompt }];
|
|
121
|
+
userInput = opts.context?.trim()
|
|
122
|
+
? `Task context (authoritative; do not rediscover):\n${opts.context.slice(0, 4000)}\n\nSub-task:\n${opts.prompt}`
|
|
123
|
+
: opts.prompt;
|
|
124
|
+
}
|
|
88
125
|
// 静默 hooks:缓冲中间过程到 transcript,不写主屏。
|
|
89
126
|
let transcript = '';
|
|
90
127
|
const buf = [];
|
|
@@ -148,7 +185,9 @@ export async function spawnAgent(opts) {
|
|
|
148
185
|
if (status)
|
|
149
186
|
batch.setBatchLabel(id, status === 'complete'
|
|
150
187
|
? t('subagent.complete')
|
|
151
|
-
: status === 'failed'
|
|
188
|
+
: status === 'failed'
|
|
189
|
+
? t('subagent.failed')
|
|
190
|
+
: t('subagent.running'));
|
|
152
191
|
// 收尾:清除运行态标志,摘要行图标从「运行中 ◐」切回完成态 ●。
|
|
153
192
|
batch.setBatchRunning(id, false);
|
|
154
193
|
batch.endBatch(id, liveLayout());
|
|
@@ -168,7 +207,7 @@ export async function spawnAgent(opts) {
|
|
|
168
207
|
if (s)
|
|
169
208
|
lastChar = s[s.length - 1];
|
|
170
209
|
},
|
|
171
|
-
onToolCall: (
|
|
210
|
+
onToolCall: (_name) => {
|
|
172
211
|
if (lastChar && lastChar !== '\n') {
|
|
173
212
|
writeBuf('\n');
|
|
174
213
|
lastChar = '\n';
|
|
@@ -192,8 +231,8 @@ export async function spawnAgent(opts) {
|
|
|
192
231
|
if (preview)
|
|
193
232
|
writeBuf(` ↳ ${preview}\n`);
|
|
194
233
|
if (liveBatchId) {
|
|
195
|
-
// 子 agent
|
|
196
|
-
//
|
|
234
|
+
// 子 agent 批不展示 diff(其写入已直接落工作区,由主 agent 的当前回滚事务统一追踪);
|
|
235
|
+
// 空 preview 用占位标记 entry 已完成,避免折叠后摘要仍显示"运行中"。
|
|
197
236
|
batch.recordResult(liveBatchId, tc.name, preview || t('toolSummary.noOutput'), null, output, isToolErrorOutput(output));
|
|
198
237
|
batch.showLiveBatch(liveBatchId, liveLayout());
|
|
199
238
|
}
|
|
@@ -240,57 +279,33 @@ export async function spawnAgent(opts) {
|
|
|
240
279
|
// 每个子 agent 独享统计/预算状态。不能保存再恢复模块级单例:多个 task 并发时
|
|
241
280
|
// save/restore 会竞态,且 lastEstimate / schedulerLog 仍会污染主 agent。
|
|
242
281
|
const localContextState = createContextState();
|
|
243
|
-
|
|
244
|
-
|
|
282
|
+
// 与主 agent 完全同源:写操作直接落在工作区,进入主 agent 当前轮次的同一回滚事务
|
|
283
|
+
// (spawn 不调 beginTurn)。没有 overlay 拷贝/ChangeSet 合并这一步——那是旧 read/write
|
|
284
|
+
// 双模式的产物,子 agent 不再受限,也就不需要"先隔离再合并"。
|
|
285
|
+
const result = await runAgentCore({
|
|
245
286
|
history,
|
|
246
|
-
userInput
|
|
287
|
+
userInput,
|
|
247
288
|
signal: opts.signal,
|
|
248
289
|
hooks,
|
|
249
290
|
maxSteps,
|
|
250
291
|
toolsOverride,
|
|
292
|
+
runtimeAllowedToolNames,
|
|
251
293
|
contextState: localContextState,
|
|
252
294
|
suppressOpeningAnalysis: true, // 子代理不注入「开场分析」:仅主线面对用户的首次响应用
|
|
253
|
-
// 子代理不注入主会话「会话状态」(plan + 笔记)
|
|
254
|
-
//
|
|
255
|
-
// 灌主计划只会干扰窄 worker 并白付 token。
|
|
295
|
+
// 子代理不注入主会话「会话状态」(plan + 笔记):那是主 agent 的工作面,委派消息已带齐
|
|
296
|
+
// 子任务所需上下文,重复注入只白付 token。
|
|
256
297
|
suppressSessionState: true,
|
|
257
|
-
onToolOutcome: (tool, args) => {
|
|
258
|
-
if (tool === 'read_file' && typeof args.path === 'string')
|
|
259
|
-
readSet.add(args.path);
|
|
260
|
-
else if (['glob', 'grep'].includes(tool))
|
|
261
|
-
readSet.add('workspace');
|
|
262
|
-
},
|
|
263
298
|
});
|
|
264
|
-
let result;
|
|
265
|
-
let changeSet = null;
|
|
266
|
-
let mergeStatus = 'committed';
|
|
267
|
-
if (opts.mode === 'write') {
|
|
268
|
-
const isolated = await inOverlay(run);
|
|
269
|
-
result = isolated.value;
|
|
270
|
-
changeSet = isolated.changeSet;
|
|
271
|
-
const declared = new Set((opts.writeSet ?? []).map((item) => item.replaceAll('\\', '/').toLowerCase()));
|
|
272
|
-
const outsideDeclaration = declared.size > 0 && changeSet?.changes.some((change) => !declared.has(change.path.replaceAll('\\', '/').toLowerCase()));
|
|
273
|
-
if (!result.completed || outsideDeclaration)
|
|
274
|
-
mergeStatus = 'failed';
|
|
275
|
-
else
|
|
276
|
-
mergeStatus = await mergeSubAgentChangeSet(changeSet, opts.signal);
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
result = await run();
|
|
280
|
-
}
|
|
281
299
|
const status = opts.signal?.aborted || result.terminationReason === 'aborted'
|
|
282
300
|
? 'aborted'
|
|
283
|
-
:
|
|
284
|
-
|
|
285
|
-
|
|
301
|
+
: result.completed
|
|
302
|
+
? 'completed'
|
|
303
|
+
: 'failed';
|
|
286
304
|
return {
|
|
287
305
|
summary: result.finalText,
|
|
288
306
|
completed: result.completed && status === 'completed',
|
|
289
307
|
transcript: truncateDisplay(transcript, 20000), // 防过大;调试用,回灌主 history 的是 summary 不是 transcript
|
|
290
308
|
status,
|
|
291
|
-
findings: result.finalText ? [result.finalText] : [],
|
|
292
|
-
readSet: [...readSet].sort(),
|
|
293
|
-
changeSet,
|
|
294
309
|
usage: {
|
|
295
310
|
promptTokens: result.usage?.promptTokens ?? 0,
|
|
296
311
|
completionTokens: result.usage?.completionTokens ?? 0,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent 核心循环的工具辅助纯函数(从 core.ts 提取,2.0 步骤2 阶段A)。
|
|
3
|
+
*
|
|
4
|
+
* 这些函数不依赖 runAgentCore 的循环局部状态,只接受显式参数,故可安全模块化。
|
|
5
|
+
* 它们覆盖:工具参数解析、文件编辑恢复提示、正文噪声判定、并发/锁调度分类、
|
|
6
|
+
* 权限拒绝结果构造、mutation diff 旧内容预读、tool 结果回灌 history。
|
|
7
|
+
*
|
|
8
|
+
* 依赖说明:readDiffContext 用 jailResolve 直接 import(沙箱域,留待工具系统抽包统一处理);
|
|
9
|
+
* pushToolResult 依赖 context/relevance/lifecycle/scheduler 的 metadata 登记,不改写历史正文。
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { findTool, getToolCapabilities } from '../tools/registry.js';
|
|
13
|
+
import { jailResolve } from '../sandbox/index.js';
|
|
14
|
+
import { contextState } from '../session/compact.js';
|
|
15
|
+
import { capToolResultForHistory } from '../session/compact.js';
|
|
16
|
+
import { recordArtifact, knownEditTargets } from '../context/index.js';
|
|
17
|
+
import { isToolResultSuccess } from '../context/utils.js';
|
|
18
|
+
/** 解析工具 arguments JSON;非法或空返 null(调用方据此降级到普通 preview)。 */
|
|
19
|
+
export function parseArgs(raw) {
|
|
20
|
+
try {
|
|
21
|
+
return raw.trim() ? JSON.parse(raw) : {};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** 需要「已知编辑目标」恢复提示的文件编辑工具;其余工具的参数报错不注入该提示。 */
|
|
28
|
+
const EDIT_HINT_TOOLS = new Set(['edit_file', 'write_file']);
|
|
29
|
+
/** 文件编辑工具参数校验失败时的恢复提示:把系统已知仍新鲜的「最近 read_file 的
|
|
30
|
+
* path + hash」直接递给模型照抄,替代其在长上下文里凭记忆复述。
|
|
31
|
+
* 只展示事实、不替模型填值;没有候选(从未 read_file / 全部已失效)返 undefined。 */
|
|
32
|
+
export function argumentErrorHint(name, state) {
|
|
33
|
+
if (!EDIT_HINT_TOOLS.has(name))
|
|
34
|
+
return undefined;
|
|
35
|
+
const targets = knownEditTargets(state);
|
|
36
|
+
if (targets.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const lines = targets.map((target) => ` path=${target.path} expected_hash=${target.hash}`).join('\n');
|
|
39
|
+
return ('系统已知最近 read_file 且尚未被修改的文件(直接复制下面的 path / expected_hash,勿凭记忆复述):\n' +
|
|
40
|
+
`${lines}\n如目标文件不在其中,先 read_file 该文件再发起编辑。`);
|
|
41
|
+
}
|
|
42
|
+
/** 判定 assistant content 是否只是 "Tool results:" 这类工具结果前缀噪声。
|
|
43
|
+
* 部分模型(如 Claude)会在 tool_calls 前输出此种无意义过渡文本,写入 history
|
|
44
|
+
* 会污染后续轮次上下文并在 TUI 上泄露为孤立行。 */
|
|
45
|
+
export function isToolResultsNoise(content) {
|
|
46
|
+
return /^(?:\s*Tool results:\s*)+$/i.test(content.trim());
|
|
47
|
+
}
|
|
48
|
+
/** 只有显式声明 parallel 且无需权限确认的工具才进入普通并发组。 */
|
|
49
|
+
export function isParallelTool(name) {
|
|
50
|
+
const tool = findTool(name);
|
|
51
|
+
return !!tool && (tool.risk ?? 'safe') === 'safe' && getToolCapabilities(tool).concurrency === 'parallel';
|
|
52
|
+
}
|
|
53
|
+
/** resource-locked 工具先顺序完成权限预检,再依赖 canonical resource lock 并发执行。 */
|
|
54
|
+
export function isResourceLockedTool(name) {
|
|
55
|
+
const tool = findTool(name);
|
|
56
|
+
return !!tool && getToolCapabilities(tool).concurrency === 'resource-locked';
|
|
57
|
+
}
|
|
58
|
+
export function isResourceLockedCall(call) {
|
|
59
|
+
// sub-agent 是长时全域操作(嵌套 agent 与主 agent 同权,可写任意文件/跑任意命令),
|
|
60
|
+
// 不进 mutation 并发批:逐个串行执行,避免两个子 agent 同时改工作区。
|
|
61
|
+
return call.name !== 'sub-agent' && isResourceLockedTool(call.name);
|
|
62
|
+
}
|
|
63
|
+
/** 权限拒绝时的结构化 ToolOutcome(供调度器统一回灌,不抛错中断循环)。 */
|
|
64
|
+
export function deniedOutcome(name) {
|
|
65
|
+
return {
|
|
66
|
+
status: 'denied',
|
|
67
|
+
code: 'PERMISSION_DENIED',
|
|
68
|
+
retryable: false,
|
|
69
|
+
output: `错误:用户拒绝了工具 ${name} 的执行。`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** mutation 执行前读旧内容供 diff:write_file 取整文件旧内容(不存在→null=新建),
|
|
73
|
+
* edit_file 取 old_string 起始行号(供 diff 显示真实文件行号)。读不到则 diff 退化为相对行号。
|
|
74
|
+
* 非 mutation 或参数非法返 { preWriteOld: null, editStartLine: 1 }。失败不阻断。 */
|
|
75
|
+
export function readDiffContext(tc, parsed) {
|
|
76
|
+
if (!parsed)
|
|
77
|
+
return { preWriteOld: null, editStartLine: 1 };
|
|
78
|
+
const p = String(parsed.path ?? '');
|
|
79
|
+
if (!p)
|
|
80
|
+
return { preWriteOld: null, editStartLine: 1 };
|
|
81
|
+
if (tc.name === 'write_file') {
|
|
82
|
+
try {
|
|
83
|
+
// jailResolve:沙箱越界(../../、绝对外圈、软链出圈)抛错 → catch 兜底返 null,不泄露牢外内容(TOCTOU)
|
|
84
|
+
return { preWriteOld: readFileSync(jailResolve(p), 'utf8'), editStartLine: 1 };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { preWriteOld: null, editStartLine: 1 }; // 文件不存在(新建)、不可读 或 沙箱越界(不泄露)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (tc.name === 'edit_file') {
|
|
91
|
+
// 行尾归一化:LLM 生成的 old_string 用 LF(\n),但 Windows 文件可能是 CRLF(\r\n),
|
|
92
|
+
// 不统一则 indexOf 必败、editStartLine 恒为 1。与 edit-file.ts 保持一致归一化为 LF。
|
|
93
|
+
const oldStr = String(parsed.old_string ?? '')
|
|
94
|
+
.replace(/\r\n/g, '\n')
|
|
95
|
+
.replace(/\r/g, '\n');
|
|
96
|
+
try {
|
|
97
|
+
// jailResolve:同上,沙箱越界抛错 → catch 兜底,不泄露牢外内容
|
|
98
|
+
const raw = readFileSync(jailResolve(p), 'utf8');
|
|
99
|
+
const data = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
100
|
+
const idx = oldStr ? data.indexOf(oldStr) : -1;
|
|
101
|
+
return {
|
|
102
|
+
preWriteOld: null,
|
|
103
|
+
editStartLine: idx >= 0 ? data.slice(0, idx).split('\n').length : 1,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { preWriteOld: null, editStartLine: 1 }; // 读不到:diff 退化为相对行号(含沙箱越界)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { preWriteOld: null, editStartLine: 1 };
|
|
111
|
+
}
|
|
112
|
+
/** 回灌 tool 结果到 history。
|
|
113
|
+
* 正常路径只做单条 hard cap;原始 output 同时供 TUI 展示,因此用户与模型
|
|
114
|
+
* 看到同一事实。Artifact/Relevance/Lifecycle 仅登记 metadata/provenance,
|
|
115
|
+
* 不在这里改写旧正文;所有自动清理与压缩统一由 80% pressure scheduler 决定。 */
|
|
116
|
+
export function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runtimeContextState = contextState, succeededOverride) {
|
|
117
|
+
const succeeded = succeededOverride ?? isToolResultSuccess(output);
|
|
118
|
+
const msg = {
|
|
119
|
+
role: 'tool',
|
|
120
|
+
tool_call_id: tc.id,
|
|
121
|
+
// Preserve evidence verbatim in normal operation; the hard per-result cap
|
|
122
|
+
// remains solely as a request-size safety rail.
|
|
123
|
+
content: capToolResultForHistory(tc.name, output),
|
|
124
|
+
};
|
|
125
|
+
history.push(msg);
|
|
126
|
+
const messageIndex = history.length - 1;
|
|
127
|
+
recordArtifact(runtimeContextState, history, messageIndex, output, succeeded);
|
|
128
|
+
// 失败 read 不得淘汰旧 read;失败 consumer 也不能改变 lifecycle 上游状态。
|
|
129
|
+
if (pruner)
|
|
130
|
+
pruner.observePush(history, msg, succeeded);
|
|
131
|
+
if (lifecycle)
|
|
132
|
+
lifecycle.pushTool(history, messageIndex, succeeded);
|
|
133
|
+
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
134
|
+
}
|