mocode-ai 1.6.3 → 1.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +376 -365
- package/README.zh-CN.md +15 -4
- package/dist/agent/model-turn.js +14 -1
- package/dist/agent/run-coordinator.js +129 -84
- package/dist/agent/spawn.js +38 -5
- package/dist/agent/stages/model-runner.js +1 -1
- package/dist/agent/stages/tool-dispatcher.js +117 -79
- package/dist/agent/work-discipline.js +3 -1
- package/dist/config/index.js +61 -21
- package/dist/config/presets.js +40 -1
- package/dist/config/profiles.js +9 -1
- package/dist/context/clearing.js +53 -0
- package/dist/context/encoders/passthrough.js +2 -2
- package/dist/context/pipeline.js +4 -3
- package/dist/context/relevance.js +58 -13
- package/dist/context/text-search.js +113 -0
- package/dist/llm/index.js +35 -8
- package/dist/llm/providers/anthropic.js +14 -4
- package/dist/llm/reasoning.js +141 -0
- package/dist/memory/reflect.js +1 -1
- package/dist/memory/store.js +31 -28
- package/dist/models/budgets.js +11 -0
- package/dist/models/catalog.js +130 -0
- package/dist/models/map-preset.js +50 -0
- package/dist/models/model-panel.js +227 -0
- package/dist/models/protocol.js +44 -0
- package/dist/models/reasoning-cap.js +88 -0
- package/dist/models/search.js +58 -0
- package/dist/models/types.js +8 -0
- package/dist/repl/commands/effort.js +81 -0
- package/dist/repl/commands/model-actions.js +138 -0
- package/dist/repl/commands/model-catalog.js +67 -0
- package/dist/repl/commands/model-panel-ui.js +107 -0
- package/dist/repl/commands/model.js +17 -0
- package/dist/repl/commands/registry.js +6 -0
- package/dist/repl/commands/session.js +54 -0
- package/dist/repl/commands/stats.js +28 -0
- package/dist/repl/commands.js +3 -0
- package/dist/repl/runtime.js +5 -1
- package/dist/repl/status-bar.js +4 -2
- package/dist/rollback/store.js +17 -0
- package/dist/runtime/runtime.js +31 -1
- package/dist/session/compact.js +84 -3
- package/dist/session/index.js +2 -0
- package/dist/session/retention.js +297 -0
- package/dist/session/scheduler.js +30 -3
- package/dist/session/store.js +108 -6
- package/dist/session/usage-stats.js +90 -0
- package/dist/skills/activation.js +2 -0
- package/dist/skills/builtin-skills.js +153 -0
- package/dist/skills/discover.js +3 -1
- package/dist/skills/runner.js +4 -1
- package/dist/tools/builtins/edit-file.js +2 -4
- package/dist/tools/builtins/grep.js +2 -4
- package/dist/tools/builtins/index.js +4 -0
- package/dist/tools/builtins/read-file.js +21 -5
- package/dist/tools/builtins/session-search.js +33 -0
- package/dist/tools/builtins/task.js +2 -0
- package/dist/tools/constants.js +8 -0
- package/dist/tools/policy.js +1 -1
- package/dist/tools/read-dedup.js +62 -0
- package/dist/tools/tool-runtime.js +1 -0
- package/dist/ui/batch.js +109 -103
- package/dist/ui/fuzzy-picker.js +227 -0
- package/dist/ui/hierarchical-picker.js +302 -0
- package/dist/ui/intervention.js +11 -1
- package/dist/ui/layout-internal/statusbar.js +18 -7
- package/dist/ui/render.js +2 -0
- package/package.json +3 -2
package/README.zh-CN.md
CHANGED
|
@@ -25,6 +25,15 @@ mocode 自己探索代码、读写改文件、执行命令、联网查资料,以
|
|
|
25
25
|
|
|
26
26
|
<p align="center"><img src="./assets/demo-build-pomodoro.gif" alt="mocode 从零搭建番茄钟并通过浏览器截图自检" width="100%"></p>
|
|
27
27
|
|
|
28
|
+
## 效率:更快、更省 token
|
|
29
|
+
|
|
30
|
+
- **两级模型面板** — 裸 `/model` 先列厂商、Enter 进入后再选模型;搜索是作用域内过滤——顶层只匹配**厂商名**,厂商内只匹配**模型名**,不再把厂商和模型混在一个结果里。
|
|
31
|
+
- **思考强度 `/effort`** — `off / low / medium / high / auto` 五档归一,按目标模型自动翻译成各家方言(Anthropic / OpenAI o·gpt-5 / Qwen3 / GLM / DeepSeek-R1 / 豆包);不认识的模型或第三方网关**不下发**未知字段,避免 400。
|
|
32
|
+
- **用量统计 `/stats`** — 本会话缓存命中率、分层 token 用量与压缩次数,基于后端真实上报,不靠估算。
|
|
33
|
+
- **缓存安全的压缩分叉** — 能整段复用旧对话时优先走 fork,保住 prompt-cache 前缀、减少重复计费;不满足条件再回落摘要(`MOCODE_COMPACT_FORK=false` 关闭)。
|
|
34
|
+
- **重复读取去重** — 同一轮对同一文件的重复读取命中缓存,不再为相同内容重复付费(`MOCODE_READ_DEDUP=false` 关闭)。
|
|
35
|
+
- **子 Agent 深度闸** — 递归派生默认最多 3 层(`SUB_AGENT_MAX_DEPTH` 可调),防止子 agent 无限膨胀。
|
|
36
|
+
|
|
28
37
|
## 架构
|
|
29
38
|
|
|
30
39
|
MoCode 是一个分层的自治运行时:终端交互层驱动 Agent 内核,内核通过受控能力平面执行真实操作,持久化认知层则让长任务和跨会话工作保持连贯。
|
|
@@ -109,7 +118,7 @@ mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
|
|
|
109
118
|
- **会话持久化** — 每轮自动落盘,`--resume` / `/resume` 续接历史会话
|
|
110
119
|
- **Skills 系统** — 自动扫描 `~/.mocode/skills/` 等目录,description 注入系统提示,模型按需调 `use_skill` 加载完整指令(渐进式披露:先看简介,任务相关才加载正文)
|
|
111
120
|
- **可选桌宠** — 独立悬浮窗(`/pet`)显示一个小角色,镜像 agent 活动(空闲 / 思考 / 跑工具 / 等人工),独立进程走 WebSocket,`/pet quit` 完全关闭。挂在终端外,绝不挡终端。
|
|
112
|
-
- **斜杠命令** — `/exit` `/clear` `/cd` `/context` `/skills` `/compact` `/resume` `/rollback` `/memory` `/reflect` `/init` `/theme` `/model` `/plan` `/auto` `/pet`,输入时下拉过滤
|
|
121
|
+
- **斜杠命令** — `/exit` `/clear` `/cd` `/context` `/skills` `/compact` `/resume` `/rollback` `/memory` `/reflect` `/init` `/theme` `/model` `/effort` `/stats` `/plan` `/auto` `/pet`,输入时下拉过滤
|
|
113
122
|
|
|
114
123
|
## 使用文档
|
|
115
124
|
|
|
@@ -188,8 +197,8 @@ LLM_MODEL=glm-4.6 # 换成你的模型名
|
|
|
188
197
|
| `ANYSEARCH_API_KEY` | 联网搜索 API key(不配走匿名免费额度) | 无 |
|
|
189
198
|
| `ANYSEARCH_BASE_URL` | 搜索 API 端点 | `https://api.anysearch.com` |
|
|
190
199
|
| `SKILLS_DIRS` | 覆盖默认 skill 扫描目录(平台分隔符) | 三目录自动扫描 |
|
|
191
|
-
| `MOCODE_CONTEXT_OPTIMIZE` | 仅在真实 pressure 下编码 Cold
|
|
192
|
-
| `MOCODE_CONTEXT_RELPRUNE` | 仅在真实 pressure 下裁剪精确 superseded
|
|
200
|
+
| `MOCODE_CONTEXT_OPTIMIZE` | 仅在真实 pressure 下编码 Cold 日志/搜索(设 `false` 关闭) | `true` |
|
|
201
|
+
| `MOCODE_CONTEXT_RELPRUNE` | 仅在真实 pressure 下裁剪精确 superseded 证据(设 `false` 关闭) | `true` |
|
|
193
202
|
| `MOCODE_LIFECYCLE` | 只维护 provenance 元数据,不按次数改写正文 | `true` |
|
|
194
203
|
| `MAX_STEPS` | 每轮 Agent 循环最大步数(仅防无限循环) | `1000` |
|
|
195
204
|
| `SUB_AGENT_MAX_STEPS` | 子 Agent 循环安全上限,默认与主 Agent 一致 | `1000` |
|
|
@@ -295,7 +304,9 @@ dev_server stop id=srv-xxxx
|
|
|
295
304
|
| `/memory` | 看记忆库:条目数 + 近期索引 |
|
|
296
305
|
| `/memory_switch` | 允许/禁止 memory 自动路由并切换 Memory Index;下一真实用户轮生效 |
|
|
297
306
|
| `/reflect` | 手动触发一次后台记忆反思 pass |
|
|
298
|
-
| `/model` |
|
|
307
|
+
| `/model` | 两级面板切换模型(先选厂商再选模型;顶层按厂商名、厂商内按模型名过滤);也可配置 baseURL / apiKey / 上下文窗口,即时生效 + 持久化 |
|
|
308
|
+
| `/effort` | 设置思考强度 off/low/medium/high/auto(如 `/effort high`);未识别模型不下发参数 |
|
|
309
|
+
| `/stats` | 本会话用量:缓存命中率 / 分层 token / 压缩次数 |
|
|
299
310
|
| `/init` | 扫描项目生成 `AGENTS.md` 项目记忆(发给 agent 执行) |
|
|
300
311
|
| `/theme` | 切换颜色主题(↑↓ · Enter,或 `/theme <name>` 直切) |
|
|
301
312
|
| `/plan` | 切到 plan 模式(只读探查 + 产出计划,审批后切 auto 执行) |
|
package/dist/agent/model-turn.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { estimatePromptTokens, estimateTokens, isContextLengthError, } from '../llm/index.js';
|
|
2
2
|
import { visionBatch, visionKeep } from '../config/index.js';
|
|
3
|
+
import { getActiveSessionStore } from '../session/store.js';
|
|
4
|
+
import { getCurrentSessionId } from '../session/state.js';
|
|
5
|
+
import { toUsageRecord } from '../session/usage-stats.js';
|
|
3
6
|
/**
|
|
4
7
|
* 沿 cause 链(≤3 层)取第一个 errno,供 trace 取证。
|
|
5
8
|
* undici 把底层 errno 挂在 cause 上(`TypeError: fetch failed` → cause `read ECONNRESET`),
|
|
@@ -18,7 +21,7 @@ function traceCauseCode(err) {
|
|
|
18
21
|
}
|
|
19
22
|
/** Executes context preparation plus exactly one model step, including the single overflow retry path. */
|
|
20
23
|
export async function runModelTurn(input) {
|
|
21
|
-
const { opts, ctx, history, historyManager, runtimeContextState, scheduler, contextTrimmer, modelRunner, activeTools, runPolicy, step, cacheState, turnLifecycle, cancellationLifecycle, rebuildHistoryIndexes, } = input;
|
|
24
|
+
const { opts, ctx, history, historyManager, runtimeContextState, scheduler, contextTrimmer, modelRunner, activeTools, runPolicy, step, readDedup, cacheState, turnLifecycle, cancellationLifecycle, rebuildHistoryIndexes, } = input;
|
|
22
25
|
const { signal, onContextUpdate, hooks } = opts;
|
|
23
26
|
const { usageMeter, emitTrace } = turnLifecycle;
|
|
24
27
|
const requestBaseURL = ctx.config.baseURL;
|
|
@@ -37,6 +40,8 @@ export async function runModelTurn(input) {
|
|
|
37
40
|
// 视觉滑动窗口**必须剪在 trim 之前**:contextTrimmer.trim() 拿的是 historyManager.snapshot(),
|
|
38
41
|
// 若先 trim,预算口径还是未剪的旧数组,80% 压力线照旧被图像撑爆(design-notes/vision-window.md §2.1)。
|
|
39
42
|
// 只在这一个地方剪;下面的 overflow 重试路径读同一个 snapshot,会自动受益。
|
|
43
|
+
runtimeContextState.currentStep = step;
|
|
44
|
+
readDedup.beginStep(step);
|
|
40
45
|
const visionKeepN = visionKeep();
|
|
41
46
|
if (visionKeepN > 0 && historyManager.pruneVisionWindow({ keep: visionKeepN, batch: visionBatch(), step })) {
|
|
42
47
|
rebuildHistoryIndexes();
|
|
@@ -52,6 +57,8 @@ export async function runModelTurn(input) {
|
|
|
52
57
|
signal,
|
|
53
58
|
});
|
|
54
59
|
historyRebuilt = trimResult.kind === 'rebuild';
|
|
60
|
+
if (trimResult.kind === 'rebuild' || trimResult.kind === 'content')
|
|
61
|
+
readDedup.markContextChanged();
|
|
55
62
|
const trimStats = trimResult.kind === 'aborted' ? {} : trimResult.stats;
|
|
56
63
|
if (scheduler && trimStats.compactHistoryCalled) {
|
|
57
64
|
emitTrace('compact', {
|
|
@@ -244,6 +251,12 @@ export async function runModelTurn(input) {
|
|
|
244
251
|
});
|
|
245
252
|
runtimeContextState.lastUsage = result.usage;
|
|
246
253
|
usageMeter.add(result.usage);
|
|
254
|
+
// P1:per-step 用量落盘(失败静默,见 appendUsage)。estimatedTotal 用本次裸估算,
|
|
255
|
+
// 供 /stats 校验估算偏差。
|
|
256
|
+
const record = toUsageRecord(step, 'main', result.usage, stepPromptEst);
|
|
257
|
+
const sessionId = getCurrentSessionId();
|
|
258
|
+
if (record && sessionId)
|
|
259
|
+
getActiveSessionStore().appendUsage(sessionId, record);
|
|
247
260
|
if (result.usage) {
|
|
248
261
|
cacheState.lastStepPromptTokens = result.usage.promptTokens;
|
|
249
262
|
if (result.usage.cachedTokens > 0)
|
|
@@ -13,6 +13,7 @@ import { runToolTurn } from './tool-turn.js';
|
|
|
13
13
|
import { createTurnLifecycle } from './turn-lifecycle.js';
|
|
14
14
|
import { parseArgs, argumentErrorHint, isParallelTool, isParallelOrchestrationCall, isResourceLockedCall, deniedOutcome, readDiffContext, pushToolResult, } from './tool-helpers.js';
|
|
15
15
|
import { contextState, summarizeToolArguments } from '../session/index.js';
|
|
16
|
+
import { createReadDedup } from '../tools/read-dedup.js';
|
|
16
17
|
import { createBudgetScheduler } from '../session/scheduler.js';
|
|
17
18
|
import { invalidateArtifacts, rehydrateArtifacts } from '../context/index.js';
|
|
18
19
|
import { createRelevancePruner } from '../context/relevance.js';
|
|
@@ -64,6 +65,8 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
64
65
|
const { usageMeter, emitTrace, traceTurnId } = turnLifecycle;
|
|
65
66
|
const toolTurnPlanState = { stepsSincePlanTouch: 0 };
|
|
66
67
|
historyManager.appendUserTurn(userInput);
|
|
68
|
+
// P2:每用户 turn 一个重复读 scope,经 dispatcher → ToolContext 透传给 read_file。
|
|
69
|
+
const readDedup = createReadDedup();
|
|
67
70
|
// The initial cancellation checkpoint is captured after the user turn and before any model/tool work.
|
|
68
71
|
// Relevance and lifecycle collect provenance during normal work. Neither path
|
|
69
72
|
// rewrites history; exact supersession is applied only by the pressure scheduler.
|
|
@@ -173,6 +176,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
173
176
|
activeTools,
|
|
174
177
|
runPolicy,
|
|
175
178
|
step,
|
|
179
|
+
readDedup,
|
|
176
180
|
cacheState: modelCacheState,
|
|
177
181
|
turnLifecycle,
|
|
178
182
|
cancellationLifecycle,
|
|
@@ -318,6 +322,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
318
322
|
isDenied: isToolDeniedForStep,
|
|
319
323
|
currentAllowedToolNames,
|
|
320
324
|
delegation: delegationForOrchestrator,
|
|
325
|
+
readDedup,
|
|
321
326
|
argumentErrorHint: (name) => argumentErrorHint(name, runtimeContextState),
|
|
322
327
|
...(opts.toolPolicy
|
|
323
328
|
? {
|
|
@@ -367,100 +372,140 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
367
372
|
...(tc.id ? { providerToolCallId: tc.id } : {}),
|
|
368
373
|
});
|
|
369
374
|
};
|
|
370
|
-
const
|
|
375
|
+
const controlIndexes = [];
|
|
376
|
+
const otherIndexes = [];
|
|
377
|
+
calls.forEach((tc, index) => (tc.name === ADD_TOOL_GROUPS_TOOL_NAME ? controlIndexes : otherIndexes).push(index));
|
|
378
|
+
const hasToolRouteBarrier = controlIndexes.length > 0;
|
|
371
379
|
if (hasToolRouteBarrier) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
380
|
+
// 同批非控制调用全部为「未被禁用的并行安全(只读)工具」时(纯 solo 空集也满足),
|
|
381
|
+
// 先并发执行只读、再应用扩容;混有写/执行等非并行工具则整批保守拒绝。
|
|
382
|
+
const safeReadonlyBatch = otherIndexes.every((index) => isParallelTool(calls[index].name, ctx.toolRuntime) && !isToolDeniedForStep(calls[index].name));
|
|
383
|
+
if (safeReadonlyBatch) {
|
|
384
|
+
for (let index = 0; index < calls.length; index++) {
|
|
385
|
+
hooks.onToolHeader?.(calls[index]);
|
|
386
|
+
}
|
|
387
|
+
if (otherIndexes.length > 0) {
|
|
388
|
+
hooks.onToolStart?.(calls[otherIndexes[0]].name);
|
|
389
|
+
const startedReadonly = otherIndexes.map((index) => ctx.toolRuntime.executeToolOutcome(calls[index].name, calls[index].arguments, signal, {
|
|
390
|
+
callId: calls[index].id,
|
|
391
|
+
allowedToolNames: currentAllowedToolNames(),
|
|
392
|
+
delegation: delegationForOrchestrator(),
|
|
393
|
+
}));
|
|
394
|
+
for (let k = 0; k < otherIndexes.length; k++) {
|
|
395
|
+
const index = otherIndexes[k];
|
|
396
|
+
const tc = calls[index];
|
|
397
|
+
const outcome = await startedReadonly[k];
|
|
398
|
+
usageMeter.add(outcome.usage);
|
|
399
|
+
opts.onToolOutcome?.(tc.name, parseArgs(tc.arguments) ?? {}, outcome);
|
|
400
|
+
traceToolEnd(tc, index, outcome);
|
|
401
|
+
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
402
|
+
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
403
|
+
}
|
|
404
|
+
hooks.onToolDone?.();
|
|
405
|
+
}
|
|
406
|
+
for (const index of controlIndexes) {
|
|
407
|
+
const tc = calls[index];
|
|
408
|
+
const parsed = parseArgs(tc.arguments);
|
|
409
|
+
let outcome;
|
|
410
|
+
if (isToolDeniedForStep(tc.name)) {
|
|
411
|
+
outcome = {
|
|
412
|
+
status: 'denied',
|
|
413
|
+
code: 'TOOL_DISABLED',
|
|
414
|
+
retryable: false,
|
|
415
|
+
output: `错误:当前 tool policy snapshot 不允许调用 ${tc.name}。`,
|
|
416
|
+
changedFiles: [],
|
|
417
|
+
durationMs: 0,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
else if (!opts.toolPolicy) {
|
|
421
|
+
outcome = {
|
|
422
|
+
status: 'denied',
|
|
423
|
+
code: 'TOOL_DISABLED',
|
|
424
|
+
retryable: false,
|
|
425
|
+
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
426
|
+
changedFiles: [],
|
|
427
|
+
durationMs: 0,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
else if (!parsed ||
|
|
431
|
+
!Array.isArray(parsed.groups) ||
|
|
432
|
+
parsed.groups.length === 0 ||
|
|
433
|
+
typeof parsed.reason !== 'string' ||
|
|
434
|
+
!parsed.reason.trim()) {
|
|
435
|
+
outcome = {
|
|
436
|
+
status: 'error',
|
|
437
|
+
code: 'INVALID_ARGUMENTS',
|
|
438
|
+
retryable: false,
|
|
439
|
+
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
440
|
+
changedFiles: [],
|
|
441
|
+
durationMs: 0,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
const expansion = opts.toolPolicy.expand(parsed.groups, parsed.reason);
|
|
446
|
+
const succeeded = expansion.added.length > 0;
|
|
447
|
+
const details = [
|
|
448
|
+
succeeded
|
|
449
|
+
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
450
|
+
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
451
|
+
expansion.implied.length > 0
|
|
452
|
+
? `Implied groups also activated: ${expansion.implied.join(', ')}.`
|
|
453
|
+
: '',
|
|
454
|
+
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
455
|
+
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
456
|
+
]
|
|
457
|
+
.filter(Boolean)
|
|
458
|
+
.join('\n');
|
|
459
|
+
outcome = {
|
|
460
|
+
status: succeeded ? 'success' : 'error',
|
|
461
|
+
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
462
|
+
retryable: false,
|
|
463
|
+
output: details,
|
|
464
|
+
changedFiles: [],
|
|
465
|
+
durationMs: 0,
|
|
466
|
+
};
|
|
467
|
+
emitTrace('tool_route_expand', {
|
|
468
|
+
policyId: expansion.snapshot.id,
|
|
469
|
+
fromVersion: policySnapshot?.version,
|
|
470
|
+
toVersion: expansion.snapshot.version,
|
|
471
|
+
requestedGroups: parsed.groups.map(String),
|
|
472
|
+
addedGroups: expansion.added,
|
|
473
|
+
impliedGroups: expansion.implied,
|
|
474
|
+
rejected: expansion.rejected,
|
|
475
|
+
reason: parsed.reason,
|
|
476
|
+
status: outcome.status,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
opts.onToolOutcome?.(tc.name, parsed ?? {}, outcome);
|
|
480
|
+
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
481
|
+
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
482
|
+
traceToolEnd(tc, index, outcome);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
// 不安全批:不扩容、不执行普通工具,逐 call 按原序配对拒绝结果。
|
|
487
|
+
for (let index = 0; index < calls.length; index++) {
|
|
488
|
+
const tc = calls[index];
|
|
489
|
+
hooks.onToolHeader?.(tc);
|
|
379
490
|
const isControl = tc.name === ADD_TOOL_GROUPS_TOOL_NAME;
|
|
380
|
-
|
|
491
|
+
const isReadonly = isParallelTool(tc.name, ctx.toolRuntime);
|
|
492
|
+
const outcome = {
|
|
381
493
|
status: 'denied',
|
|
382
494
|
code: isControl ? 'INVALID_ARGUMENTS' : 'TOOL_DISABLED',
|
|
383
495
|
retryable: false,
|
|
384
496
|
output: isControl
|
|
385
|
-
? '错误:add_tool_groups
|
|
386
|
-
:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
else if (isToolDeniedForStep(tc.name)) {
|
|
392
|
-
outcome = {
|
|
393
|
-
status: 'denied',
|
|
394
|
-
code: 'TOOL_DISABLED',
|
|
395
|
-
retryable: false,
|
|
396
|
-
output: `错误:当前 tool policy snapshot 不允许调用 ${tc.name}。`,
|
|
497
|
+
? '错误:add_tool_groups 只能单独调用,或与只读工具(read_file/glob/grep/web 等)同批;本次没有扩容。'
|
|
498
|
+
: isReadonly
|
|
499
|
+
? `错误:同一响应包含 add_tool_groups,工具 ${tc.name} 未执行。请在下一 step 重试。`
|
|
500
|
+
: `错误:add_tool_groups 不能与写/执行工具 ${tc.name} 同批;请先完成扩容,再在下一 step 调用 ${tc.name}。`,
|
|
397
501
|
changedFiles: [],
|
|
398
502
|
durationMs: 0,
|
|
399
503
|
};
|
|
504
|
+
opts.onToolOutcome?.(tc.name, parseArgs(tc.arguments) ?? {}, outcome);
|
|
505
|
+
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
506
|
+
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, false);
|
|
507
|
+
traceToolEnd(tc, index, outcome);
|
|
400
508
|
}
|
|
401
|
-
else if (!opts.toolPolicy) {
|
|
402
|
-
outcome = {
|
|
403
|
-
status: 'denied',
|
|
404
|
-
code: 'TOOL_DISABLED',
|
|
405
|
-
retryable: false,
|
|
406
|
-
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
407
|
-
changedFiles: [],
|
|
408
|
-
durationMs: 0,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
else if (!parsed ||
|
|
412
|
-
!Array.isArray(parsed.groups) ||
|
|
413
|
-
parsed.groups.length === 0 ||
|
|
414
|
-
typeof parsed.reason !== 'string' ||
|
|
415
|
-
!parsed.reason.trim()) {
|
|
416
|
-
outcome = {
|
|
417
|
-
status: 'error',
|
|
418
|
-
code: 'INVALID_ARGUMENTS',
|
|
419
|
-
retryable: false,
|
|
420
|
-
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
421
|
-
changedFiles: [],
|
|
422
|
-
durationMs: 0,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
const expansion = opts.toolPolicy.expand(parsed.groups, parsed.reason);
|
|
427
|
-
const succeeded = expansion.added.length > 0;
|
|
428
|
-
const details = [
|
|
429
|
-
succeeded
|
|
430
|
-
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
431
|
-
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
432
|
-
expansion.implied.length > 0
|
|
433
|
-
? `Implied groups also activated: ${expansion.implied.join(', ')}.`
|
|
434
|
-
: '',
|
|
435
|
-
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
436
|
-
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
437
|
-
]
|
|
438
|
-
.filter(Boolean)
|
|
439
|
-
.join('\n');
|
|
440
|
-
outcome = {
|
|
441
|
-
status: succeeded ? 'success' : 'error',
|
|
442
|
-
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
443
|
-
retryable: false,
|
|
444
|
-
output: details,
|
|
445
|
-
changedFiles: [],
|
|
446
|
-
durationMs: 0,
|
|
447
|
-
};
|
|
448
|
-
emitTrace('tool_route_expand', {
|
|
449
|
-
policyId: expansion.snapshot.id,
|
|
450
|
-
fromVersion: policySnapshot?.version,
|
|
451
|
-
toVersion: expansion.snapshot.version,
|
|
452
|
-
requestedGroups: parsed.groups.map(String),
|
|
453
|
-
addedGroups: expansion.added,
|
|
454
|
-
impliedGroups: expansion.implied,
|
|
455
|
-
rejected: expansion.rejected,
|
|
456
|
-
reason: parsed.reason,
|
|
457
|
-
status: outcome.status,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
opts.onToolOutcome?.(tc.name, parsed ?? {}, outcome);
|
|
461
|
-
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
462
|
-
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
463
|
-
traceToolEnd(tc, index, outcome);
|
|
464
509
|
}
|
|
465
510
|
}
|
|
466
511
|
// add_tool_groups 是 step 屏障:只要本响应出现该控制调用,本批所有普通工具都不执行。
|
package/dist/agent/spawn.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// - 主屏渲染可选:TUI 激活时把子 agent 内部工具调用实时写入主内容区并复用 batch 折叠;
|
|
11
11
|
// TUI 未激活(host 嵌入 / 非 TTY)时纯静默,中间过程只缓冲进 transcript。
|
|
12
12
|
// - 独立 history 分支:子任务的工具噪声不回灌主对话,只有最终摘要回灌。
|
|
13
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
13
14
|
import { buildMocodeCorePrompt, isSubAgentHardDisabled } from '../config/index.js';
|
|
14
15
|
import { getToolChatSchema } from '../tools/policy.js';
|
|
15
16
|
import { effectiveSystemPrompt } from '../skills/index.js';
|
|
@@ -47,6 +48,11 @@ You are executing one delegated sub-task with the same engineering standards and
|
|
|
47
48
|
* 中断:opts.signal 透传给子 runAgentCore——主 Ctrl+C 树杀子 agent(chat abort + 工具 abort)。
|
|
48
49
|
* 子 agent 跑在主 signal 下,主 abort 即子 abort;子 agent 的 abortRestore 还原子 history + 模式。
|
|
49
50
|
*/
|
|
51
|
+
/** 当前委派深度(P4 闸2):根 agent 无 store=0,每进一层 spawnAgent +1。 */
|
|
52
|
+
const spawnDepthScope = new AsyncLocalStorage();
|
|
53
|
+
function currentSpawnDepth() {
|
|
54
|
+
return spawnDepthScope.getStore() ?? 0;
|
|
55
|
+
}
|
|
50
56
|
export async function spawnAgent(opts) {
|
|
51
57
|
const activeRuntime = opts.runtime ?? getActiveRuntime();
|
|
52
58
|
const runtimeContext = opts.runtimeContext ?? activeRuntime?.context ?? getActiveAgentRuntimeContext() ?? defaultAgentRuntimeContext;
|
|
@@ -60,6 +66,20 @@ export async function spawnAgent(opts) {
|
|
|
60
66
|
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, reasoningTokens: 0 },
|
|
61
67
|
};
|
|
62
68
|
}
|
|
69
|
+
// 深度闸:达上限直接结构化失败(模型看到后会改用直接工具完成)。执行层拦截,不裁 schema。
|
|
70
|
+
const depth = currentSpawnDepth() + 1;
|
|
71
|
+
if (depth > runtimeContext.config.subAgentMaxDepth) {
|
|
72
|
+
const msg = `Sub-agent depth limit ${runtimeContext.config.subAgentMaxDepth} reached ` +
|
|
73
|
+
'(nested delegation blocked to prevent fork-style recursion); complete the task directly with your own tools.';
|
|
74
|
+
return {
|
|
75
|
+
summary: msg,
|
|
76
|
+
completed: false,
|
|
77
|
+
transcript: msg,
|
|
78
|
+
status: 'failed',
|
|
79
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, reasoningTokens: 0 },
|
|
80
|
+
changedFiles: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
63
83
|
const maxSteps = opts.maxSteps ?? runtimeContext.config.subAgentMaxSteps;
|
|
64
84
|
const requested = opts.tools === undefined ? null : new Set(opts.tools);
|
|
65
85
|
const shared = opts.delegation && opts.delegation.history.length > 0 && opts.delegation.history[0]?.role === 'system'
|
|
@@ -172,8 +192,8 @@ export async function spawnAgent(opts) {
|
|
|
172
192
|
const childIndex = parentId ? batch.getGroupChildIndex(opts.callId) : undefined;
|
|
173
193
|
liveBatchId = batch.beginBatch(t('subagent.running'), {
|
|
174
194
|
parentId,
|
|
175
|
-
//
|
|
176
|
-
|
|
195
|
+
// 缩进由 beginBatch 按父层级自动推导:组容器批下 = 两层 entry 缩进;
|
|
196
|
+
// 普通子 agent 批下再嵌套 = 父缩进 + 两层(树状逐层加深)。
|
|
177
197
|
groupChildIndex: childIndex,
|
|
178
198
|
running: true,
|
|
179
199
|
});
|
|
@@ -205,6 +225,8 @@ export async function spawnAgent(opts) {
|
|
|
205
225
|
layout.contentWrite('\n');
|
|
206
226
|
};
|
|
207
227
|
let lastChar = '';
|
|
228
|
+
// P4 闸3:只收集本 agent 树自己产生的改动(钩子按真实执行触发),不靠整轮全局快照。
|
|
229
|
+
const nestedChangedFiles = new Set();
|
|
208
230
|
const hooks = {
|
|
209
231
|
onText: (s) => {
|
|
210
232
|
writeBuf(s); // 缓冲流式正文(无 markdown 渲染,原始文本)
|
|
@@ -225,7 +247,10 @@ export async function spawnAgent(opts) {
|
|
|
225
247
|
ensureQuietLine(opts.quietLabel ?? t('skill.executing', { name: opts.prompt.slice(0, 40) }));
|
|
226
248
|
ensureLiveBatch();
|
|
227
249
|
if (liveBatchId) {
|
|
228
|
-
|
|
250
|
+
// 登记内部调用的 tool_call id:子 agent 嵌套派生时,孙 spawnAgent 据此反查到
|
|
251
|
+
// 本子批,把孙批挂到对应的内部 sub-agent entry 下(而非游离到 buffer 末尾)。
|
|
252
|
+
batch.bindCall(tc.id, liveBatchId);
|
|
253
|
+
batch.recordCall(liveBatchId, tc.name, summary, tc.id);
|
|
229
254
|
// 默认折叠,只刷新摘要行计数(glob/read_file 数量),不展开明细列表。
|
|
230
255
|
batch.showLiveBatch(liveBatchId, liveLayout());
|
|
231
256
|
}
|
|
@@ -237,7 +262,7 @@ export async function spawnAgent(opts) {
|
|
|
237
262
|
if (liveBatchId) {
|
|
238
263
|
// 子 agent 批不展示 diff(其写入已直接落工作区,由主 agent 的当前回滚事务统一追踪);
|
|
239
264
|
// 空 preview 用占位标记 entry 已完成,避免折叠后摘要仍显示"运行中"。
|
|
240
|
-
batch.recordResult(liveBatchId, tc.name, preview || t('toolSummary.noOutput'), null, output, isToolErrorOutput(output));
|
|
265
|
+
batch.recordResult(liveBatchId, tc.name, preview || t('toolSummary.noOutput'), null, output, isToolErrorOutput(output), tc.id);
|
|
241
266
|
batch.showLiveBatch(liveBatchId, liveLayout());
|
|
242
267
|
}
|
|
243
268
|
},
|
|
@@ -286,7 +311,7 @@ export async function spawnAgent(opts) {
|
|
|
286
311
|
// 与主 agent 完全同源:写操作直接落在工作区,进入主 agent 当前轮次的同一回滚事务
|
|
287
312
|
// (spawn 不调 beginTurn)。没有 overlay 拷贝/ChangeSet 合并这一步——那是旧 read/write
|
|
288
313
|
// 双模式的产物,子 agent 不再受限,也就不需要"先隔离再合并"。
|
|
289
|
-
const
|
|
314
|
+
const runResult = () => runtime.run({
|
|
290
315
|
turn: 'inherit',
|
|
291
316
|
history,
|
|
292
317
|
userInput,
|
|
@@ -296,11 +321,18 @@ export async function spawnAgent(opts) {
|
|
|
296
321
|
toolsOverride,
|
|
297
322
|
runtimeAllowedToolNames,
|
|
298
323
|
contextState: localContextState,
|
|
324
|
+
// P4 闸3:run-coordinator 只消费顶层 onToolOutcome(非 hooks),在此收集本树真实改动。
|
|
325
|
+
onToolOutcome: (_tool, _args, outcome) => {
|
|
326
|
+
for (const f of outcome.changedFiles ?? [])
|
|
327
|
+
nestedChangedFiles.add(f);
|
|
328
|
+
},
|
|
299
329
|
suppressOpeningAnalysis: true, // 子代理不注入「开场分析」:仅主线面对用户的首次响应用
|
|
300
330
|
// 子代理不注入主会话「会话状态」(plan + 笔记):那是主 agent 的工作面,委派消息已带齐
|
|
301
331
|
// 子任务所需上下文,重复注入只白付 token。
|
|
302
332
|
suppressSessionState: true,
|
|
303
333
|
});
|
|
334
|
+
// 深度 scope:子 agent 内再派生子 agent 时 ALS 读到此 depth。
|
|
335
|
+
const result = await spawnDepthScope.run(depth, runResult);
|
|
304
336
|
const status = opts.signal?.aborted || result.terminationReason === 'aborted'
|
|
305
337
|
? 'aborted'
|
|
306
338
|
: result.completed
|
|
@@ -318,5 +350,6 @@ export async function spawnAgent(opts) {
|
|
|
318
350
|
cachedTokens: result.usage?.cachedTokens ?? 0,
|
|
319
351
|
reasoningTokens: result.usage?.reasoningTokens ?? 0,
|
|
320
352
|
},
|
|
353
|
+
changedFiles: [...nestedChangedFiles],
|
|
321
354
|
};
|
|
322
355
|
}
|
|
@@ -7,7 +7,7 @@ class ChatModelRunner {
|
|
|
7
7
|
this.transport = transport;
|
|
8
8
|
}
|
|
9
9
|
run(request, signal) {
|
|
10
|
-
return this.transport(request.history.slice(), request.handlers, signal, request.tools.slice());
|
|
10
|
+
return this.transport(request.history.slice(), request.handlers, signal, request.tools.slice(), request.reasoningEffort ? { reasoningEffort: request.reasoningEffort } : undefined);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
class LegacyChatModelRunner extends ChatModelRunner {
|