mocode-ai 0.7.0 → 0.7.1
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 +4 -3
- package/README.zh-CN.md +3 -3
- package/dist/agent/core.js +162 -108
- package/dist/agent/index.js +20 -0
- package/dist/agent/spawn.js +9 -1
- package/dist/config/index.js +12 -0
- package/dist/i18n/index.js +34 -0
- package/dist/llm/index.js +6 -2
- package/dist/mcp/index.js +15 -2
- package/dist/permissions/index.js +110 -93
- package/dist/repl/index.js +39 -5
- package/dist/rollback/index.js +31 -0
- package/dist/session/index.js +1 -0
- package/dist/session/trace.js +26 -0
- package/dist/tools/builtins/index.js +35 -4
- package/dist/tools/builtins/run-command.js +115 -66
- package/dist/tools/builtins/task.js +5 -2
- package/dist/tools/constants.js +5 -1
- package/dist/tools/registry.js +108 -25
- package/dist/verification/affected.js +149 -0
- package/dist/verification/discovery.js +40 -0
- package/dist/verification/index.js +163 -0
- package/dist/verification/profile.js +237 -0
- package/dist/verification/types.js +1 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
|
|
|
14
14
|
|
|
15
15
|
- **Autonomous multi-step execution** — In a single conversation, the agent chains multiple steps on its own: read code, edit code, run tests, fix based on errors, and so on. It decides the next step without you nagging it. When it hits a decision point, it calls `ask_human` to pop up a panel and ask you (blocking until you respond).
|
|
16
16
|
- **Parallel read-only tools** — Consecutive read-only operations in a turn (reading files, grep, glob, codegraph, web search/fetch) run concurrently, so total time is roughly the slowest single call instead of the sum of all of them. Operations with side effects (writing/editing files) stay sequential to preserve snapshot ordering and data safety.
|
|
17
|
-
- **Sub-agents divide and conquer** — Complex tasks can spawn independent sub-agents, each with its own conversation history (isolated from the main thread), an optional restricted toolset, and a step cap.
|
|
17
|
+
- **Sub-agents divide and conquer** — Complex tasks can spawn independent sub-agents, each with its own conversation history (isolated from the main thread), an optional restricted toolset, and a step cap. Sub-agent calls execute serially while they share the main workspace, preventing concurrent writes from racing; each returns only a summary to the main thread.
|
|
18
18
|
- **Plan / Auto dual mode** — In `plan` mode the agent is read-only (reads code, queries indexes, searches — never writes to disk, runs commands, or spawns sub-agents) and produces a plan; `auto` mode unlocks the full toolset. The agent can switch between the two on its own — scope out an unfamiliar codebase first, then start making changes.
|
|
19
19
|
- **Automatic context compression** — As the context window fills up, a three-tier compression kicks in (trim individual results → compact older tool results in place → summarize older turns), so long sessions never overflow. `/context` shows live token usage; `/compact` triggers manual compression (optionally with a focus hint to preserve what matters).
|
|
20
20
|
- **Cross-session long-term memory** — The agent can save project architecture, conventions, and lessons learned as long-term memory, auto-loaded in future sessions. A background process periodically reflects on conversations to mine things worth remembering. Memories can be created, searched, updated, and forgotten, with recall-based decay.
|
|
@@ -105,6 +105,7 @@ Common backend `base_url` values:
|
|
|
105
105
|
| `COMPACT_THRESHOLD` | Auto-compaction trigger threshold (fraction of window) | `0.85` |
|
|
106
106
|
| `LLM_STREAM_USAGE` | Include `stream_options.include_usage` on streaming requests for real usage | `true` |
|
|
107
107
|
| `AUTO_COMPACT` | Auto-compaction master switch | `true` |
|
|
108
|
+
| `MOCODE_AUTO_VALIDATE` | Auto-run the lowest-cost discovered validation command after code changes; feed failures back to the agent | `true` |
|
|
108
109
|
| `AUTO_REFLECT` | Background reflection pass master switch (periodically mines memories from conversations) | `true` |
|
|
109
110
|
| `REFLECT_EVERY_N` | Trigger a background reflection every N turns (runs alongside the agent, non-blocking) | `5` |
|
|
110
111
|
| `ANYSEARCH_API_KEY` | Web search API key (falls back to anonymous free quota if unset) | none |
|
|
@@ -148,7 +149,7 @@ The agent operates in **the working directory it was launched from** — to have
|
|
|
148
149
|
| `ask_human` | Pop up a Q&A panel at decision points; user picks a preset or types freely (blocks until answered) |
|
|
149
150
|
| `switch_mode` | Switch between `plan` (read-only planning) and `auto` (full execution); the agent can call this itself to explore before acting |
|
|
150
151
|
| `drop_context` | Replace irrelevant old tool results in history with stubs to free up context (preserves tool_call_id pairing, leaves system prompt and current turn untouched, idempotent) |
|
|
151
|
-
| `task` | Spawn a sub-agent for an independent subtask (isolated history, optional restricted toolset, optional step cap);
|
|
152
|
+
| `task` | Spawn a sub-agent for an independent subtask (isolated history, optional restricted toolset, optional step cap); calls run serially while sharing the workspace and return only a summary |
|
|
152
153
|
|
|
153
154
|
| `memory_save` | Save a piece of cross-session long-term memory (title indexed, body fetched on demand) |
|
|
154
155
|
| `memory_search` | Search memory bodies by keyword; hits boost the recall count (affects forgetting decay) |
|
|
@@ -239,4 +240,4 @@ npm run typecheck # tsc --noEmit
|
|
|
239
240
|
|
|
240
241
|
## Future extensions
|
|
241
242
|
|
|
242
|
-
MCP tool integration,
|
|
243
|
+
MCP tool integration, finer-grained capability locks, and a real worktree-isolated sub-agent mode. The current version is a streaming, reasoning-visible, rollback-capable terminal coding agent with 20 tools, working-notepad planning, cross-session memory, capability-aware tool scheduling, serial workspace-sharing sub-agents, and an optional desktop pet.
|
package/README.zh-CN.md
CHANGED
|
@@ -14,7 +14,7 @@ mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
|
|
|
14
14
|
|
|
15
15
|
- **自主多步推进** — 一次对话里连续多步:读代码、改代码、跑测试、根据报错再改……agent 自己决定下一步,中途不用你反复催。遇到卡点会调 `ask_human` 弹面板问你(阻塞到回应)。
|
|
16
16
|
- **只读工具并行执行** — 一轮里连续的只读操作(读文件、grep、glob、codegraph、联网搜索/抓取)自动并发跑,总耗时 ≈ 最慢一个,而不是逐个排队。写文件 / 改文件这类有副作用的操作仍串行,保快照顺序与数据安全。
|
|
17
|
-
- **子 agent 分而治之** — 复杂任务可派生独立子 agent:各自有自己的对话历史(不污染主线)
|
|
17
|
+
- **子 agent 分而治之** — 复杂任务可派生独立子 agent:各自有自己的对话历史(不污染主线),可限定工具集和步数上限。共享主工作区期间多个 task 串行执行,避免并发写冲突;每个子任务最后只把摘要回灌主线。
|
|
18
18
|
- **计划 / 执行双模式** — `plan` 模式下只读探查(读代码、查索引、搜索,绝不写盘、不跑命令、不派生子 agent),产出计划;`auto` 模式全量工具放开。agent 还能在两者间自切换——先把陌生代码库摸清,再动手改。
|
|
19
19
|
- **上下文自动压缩** — 接近窗口上限时三层压缩(单条结果裁剪 → 旧工具结果原地微压缩 → 旧对话摘要),长会话也不爆窗口;`/context` 实时显示 token 用量,`/compact` 可手动压缩(能带焦点指令聚焦保留)。
|
|
20
20
|
- **跨会话长期记忆** — agent 能把项目架构、约定、踩过的坑存成长期记忆,下次会话自动加载;后台还会定期从对话里反思挖掘值得记住的事。记忆可增删改、带召回衰减。
|
|
@@ -147,7 +147,7 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
|
|
|
147
147
|
| `ask_human` | 决策点弹终端问答面板,用户选预设项或自由输入(阻塞至回应) |
|
|
148
148
|
| `switch_mode` | 在 `plan`(只读规划)与 `auto`(全量执行)间切换;agent 可自行调用,先探查再动手 |
|
|
149
149
|
| `drop_context` | 把历史里无关的旧工具结果替换为存根释放上下文(保 tool_call_id 配对,不动 system 与当前轮;幂等) |
|
|
150
|
-
| `task` | 派生子 agent 执行独立子任务(独立历史、可受限工具集、可设步数上限)
|
|
150
|
+
| `task` | 派生子 agent 执行独立子任务(独立历史、可受限工具集、可设步数上限);共享工作区期间串行执行,只回摘要 |
|
|
151
151
|
|
|
152
152
|
| `memory_save` | 存一条跨会话长期记忆(标题进索引,正文按需取) |
|
|
153
153
|
| `memory_search` | 按关键词搜记忆正文,命中即提升召回计数(影响遗忘衰减) |
|
|
@@ -221,4 +221,4 @@ npm run typecheck # tsc --noEmit
|
|
|
221
221
|
|
|
222
222
|
## 可后续扩展
|
|
223
223
|
|
|
224
|
-
MCP
|
|
224
|
+
MCP 工具集成、更细粒度的 capability 资源锁、真·worktree 隔离的子 agent 模式。当前版本已是流式、思考可见、可回滚的终端编码 agent:20 个工具、工作记事本规划、跨会话记忆、能力感知工具调度、共享工作区串行子 agent、可选桌宠。
|
package/dist/agent/core.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// spawn.ts 的 spawnAgent = runAgentCore + 静默 hooks(子 agent)。
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { chat, estimatePromptTokens, planChatTools, chatTools, } from '../llm/index.js';
|
|
9
|
-
import {
|
|
9
|
+
import { executeToolOutcome, getToolCapabilities, isFileMutationTool, tools, } from '../tools/registry.js';
|
|
10
10
|
import { checkPermission } from '../permissions/index.js';
|
|
11
|
-
import { getPlanDisabledTools } from '../tools/constants.js';
|
|
11
|
+
import { getPlanDisabledTools, getRuntimeDisabledTools } from '../tools/constants.js';
|
|
12
12
|
import { getAgentMode, setAgentMode } from './mode.js';
|
|
13
13
|
import { maybeCompact, contextState, dropContextFromHistory } from '../session/index.js';
|
|
14
14
|
import { createBudgetScheduler } from '../session/scheduler.js';
|
|
@@ -17,9 +17,12 @@ import { createAgeAwareEncodingState, } from '../context/age-aware.js';
|
|
|
17
17
|
import { createRelevancePruner } from '../context/relevance.js';
|
|
18
18
|
import { isToolResultSuccess } from '../context/utils.js';
|
|
19
19
|
import { config } from '../config/index.js';
|
|
20
|
+
import { t } from '../i18n/index.js';
|
|
20
21
|
import { jailResolve } from '../sandbox/index.js';
|
|
21
22
|
import { createLifecycleEngine } from '../context/lifecycle.js';
|
|
22
23
|
import { getTokenCalibration, updateTokenCalibration, } from '../context/token-calibration.js';
|
|
24
|
+
import { getCurrentTurnMutationState } from '../rollback/index.js';
|
|
25
|
+
import { runAutomaticValidation, } from '../verification/index.js';
|
|
23
26
|
/** Stable per-history age state survives user turns; WeakMap avoids retaining closed sessions. */
|
|
24
27
|
const ageAwareStateByHistory = new WeakMap();
|
|
25
28
|
function ageAwareStateFor(history) {
|
|
@@ -57,17 +60,22 @@ function thrashHint(name, args, count) {
|
|
|
57
60
|
'- edit_file → old_string mismatch; re-read the file to find the exact text\n' +
|
|
58
61
|
'- otherwise → re-read the tool description; the argument shape may be wrong');
|
|
59
62
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
/** 只有显式声明 parallel 且无需权限确认的工具才可并发;未知扩展保守串行。 */
|
|
64
|
+
function isParallelTool(name) {
|
|
65
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
66
|
+
return !!tool && (tool.risk ?? 'safe') === 'safe' &&
|
|
67
|
+
getToolCapabilities(tool).concurrency === 'parallel';
|
|
68
|
+
}
|
|
69
|
+
/** 文件 mutation 由 capability metadata 判定,供 diff、回滚与上下文失效共用。 */
|
|
70
|
+
const isMutationTool = (name) => isFileMutationTool(name);
|
|
71
|
+
function deniedOutcome(name) {
|
|
72
|
+
return {
|
|
73
|
+
status: 'denied',
|
|
74
|
+
code: 'PERMISSION_DENIED',
|
|
75
|
+
retryable: false,
|
|
76
|
+
output: `错误:用户拒绝了工具 ${name} 的执行。`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
71
79
|
/** mutation 执行前读旧内容供 diff:write_file 取整文件旧内容(不存在→null=新建),
|
|
72
80
|
* edit_file 取 old_string 起始行号(供 diff 显示真实文件行号)。读不到则 diff 退化为相对行号。
|
|
73
81
|
* 非 mutation 或参数非法返 { preWriteOld: null, editStartLine: 1 }。失败不阻断。 */
|
|
@@ -116,8 +124,8 @@ function readDiffContext(tc, parsed) {
|
|
|
116
124
|
* - lifecycle 也在每个 runAgentCore 实例化一次,登记 grep/glob/codegraph 等 producer
|
|
117
125
|
* 与 read/edit/write 的 consumer 关系;孤立+老化自动 STUB(观察类工具永不到 STUB)。
|
|
118
126
|
* - 开关关闭时 lifecycle=null 完全跳过。 */
|
|
119
|
-
function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runtimeContextState = contextState) {
|
|
120
|
-
const succeeded = isToolResultSuccess(output);
|
|
127
|
+
function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runtimeContextState = contextState, succeededOverride) {
|
|
128
|
+
const succeeded = succeededOverride ?? isToolResultSuccess(output);
|
|
121
129
|
const ageAware = config.contextOptimize ? ageAwareStateFor(history) : null;
|
|
122
130
|
const encodingContext = ageAware?.preparePush(tc, succeeded);
|
|
123
131
|
const msg = {
|
|
@@ -158,6 +166,11 @@ export async function runAgentCore(opts) {
|
|
|
158
166
|
// 本轮计时:从入口到完毕(正常 return / 达上限),供 finally 打 ✻ Worked for 摘要行。
|
|
159
167
|
const t0 = Date.now();
|
|
160
168
|
let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
|
|
169
|
+
let traceStatus = 'error';
|
|
170
|
+
let toolCallCount = 0;
|
|
171
|
+
let latestValidation;
|
|
172
|
+
let validatedMutationVersion = getCurrentTurnMutationState().version;
|
|
173
|
+
const validator = opts.validator ?? runAutomaticValidation;
|
|
161
174
|
// 本轮 token 累计:每步 chat() 返回后把 result.usage 累加,供 onDone 摘要行 + AgentRunResult.usage
|
|
162
175
|
// 透传给 repl(显示在底栏模式 chip 右边)。未开启 include_usage 或全失败时为 undefined。
|
|
163
176
|
let turnUsage;
|
|
@@ -251,7 +264,14 @@ export async function runAgentCore(opts) {
|
|
|
251
264
|
// 上一步工具被 abort 杀(run_command/web_fetch 等)→ signal.aborted,直接还原退出,不等 maybeCompact + chat()
|
|
252
265
|
if (signal?.aborted) {
|
|
253
266
|
abortRestore();
|
|
254
|
-
|
|
267
|
+
traceStatus = 'aborted';
|
|
268
|
+
const mutation = getCurrentTurnMutationState();
|
|
269
|
+
return {
|
|
270
|
+
completed: false,
|
|
271
|
+
finalText: null,
|
|
272
|
+
validation: latestValidation,
|
|
273
|
+
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
274
|
+
};
|
|
255
275
|
}
|
|
256
276
|
// 本步只计算一次实际工具集合,调度、请求和 usage 校准必须使用完全相同的 schema。
|
|
257
277
|
const activeTools = opts.toolsOverride
|
|
@@ -297,7 +317,14 @@ export async function runAgentCore(opts) {
|
|
|
297
317
|
(e instanceof Error &&
|
|
298
318
|
(e.name === 'AbortError' || e.name === 'APIUserAbortError'))) {
|
|
299
319
|
abortRestore();
|
|
300
|
-
|
|
320
|
+
traceStatus = 'aborted';
|
|
321
|
+
const mutation = getCurrentTurnMutationState();
|
|
322
|
+
return {
|
|
323
|
+
completed: false,
|
|
324
|
+
finalText: null,
|
|
325
|
+
validation: latestValidation,
|
|
326
|
+
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
327
|
+
};
|
|
301
328
|
}
|
|
302
329
|
throw e;
|
|
303
330
|
}
|
|
@@ -315,6 +342,7 @@ export async function runAgentCore(opts) {
|
|
|
315
342
|
// lastUsage 已更新:触发状态行 context 用量条重算+重画,运行中不再冻结在轮首。
|
|
316
343
|
onContextUpdate?.();
|
|
317
344
|
if (result.toolCalls.length > 0) {
|
|
345
|
+
toolCallCount += result.toolCalls.length;
|
|
318
346
|
hadToolsThisTurn = true;
|
|
319
347
|
// 流式正文末尾补换行(若 onToolCall 已补则 lastChar='\n',此处 no-op);防 ● 行黏在正文行尾
|
|
320
348
|
if (mode !== 'idle' && lastChar !== '\n')
|
|
@@ -329,104 +357,47 @@ export async function runAgentCore(opts) {
|
|
|
329
357
|
function: { name: tc.name, arguments: tc.arguments },
|
|
330
358
|
})),
|
|
331
359
|
});
|
|
332
|
-
// 工具分组执行(保 tool_calls 原顺序)
|
|
333
|
-
// 渲染全部 header,让摘要在任何同步工具真正执行前立即可见;随后启动全部
|
|
334
|
-
// 再按原顺序逐个 await +
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
// executeTool 永不抛错(调度器 try/catch 返字符串),故 await 单个 promise 不会抛(永远 resolve 为字符串)。
|
|
360
|
+
// 工具分组执行(保 tool_calls 原顺序):连续且显式声明 parallel 的 safe 工具成组并发——先一次性
|
|
361
|
+
// 渲染全部 header,让摘要在任何同步工具真正执行前立即可见;随后启动全部 executeToolOutcome,
|
|
362
|
+
// 再按原顺序逐个 await + 回灌结果。其余工具均为串行屏障;resource-locked write 保证
|
|
363
|
+
// rollback 快照顺序,未知扩展与共享工作区 task 也默认串行。
|
|
364
|
+
// 渲染与 history 回灌一律按原顺序;并发只影响执行时序,tool_call_id 仍按序配对。
|
|
365
|
+
// executeToolOutcome 永不抛错,失败通过结构化 status/code 返回。
|
|
339
366
|
const calls = result.toolCalls;
|
|
340
367
|
let i = 0;
|
|
341
368
|
while (i < calls.length) {
|
|
342
|
-
|
|
369
|
+
const currentCall = calls[i];
|
|
370
|
+
if (getRuntimeDisabledTools().has(currentCall.name)) {
|
|
371
|
+
hooks.onToolHeader?.(currentCall);
|
|
372
|
+
const error = t('task.disabled');
|
|
373
|
+
hooks.onToolResult?.(currentCall, error, null, null, 1);
|
|
374
|
+
const hint = recordAndHint(currentCall.name, currentCall.arguments);
|
|
375
|
+
pushToolResult(history, currentCall, hint ? `${error}${hint}` : error, relprune, lifecycle, scheduler, runtimeContextState, false);
|
|
376
|
+
i++;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (isParallelTool(currentCall.name)) {
|
|
343
380
|
// 收集连续只读组(≥1),并发执行:先渲染所有 header,再一次性启动所有
|
|
344
381
|
// (executeTool 调用即开始 I/O),最后按原顺序逐个 await + 回灌。
|
|
345
382
|
// 必须先 header 后 execute:grep 等同步快速工具会在 executeTool 返回 Promise 前
|
|
346
383
|
// 已经完成;若先 started.map,用户只能在工具完成后才看到摘要与其前面的换行。
|
|
347
384
|
// 异步工具(web_fetch 等)并发跑、总耗时 ≈ 最慢一个;同步工具(glob/grep)map 时已顺序跑完,await 即返。
|
|
348
385
|
let j = i;
|
|
349
|
-
while (j < calls.length &&
|
|
386
|
+
while (j < calls.length && isParallelTool(calls[j].name))
|
|
350
387
|
j++;
|
|
351
388
|
const batch = calls.slice(i, j);
|
|
352
389
|
for (const tc of batch)
|
|
353
390
|
hooks.onToolHeader?.(tc);
|
|
354
391
|
hooks.onToolStart?.(batch[0].name);
|
|
355
|
-
const started = batch.map((tc) =>
|
|
392
|
+
const started = batch.map((tc) => executeToolOutcome(tc.name, tc.arguments, signal, { dropContext }));
|
|
356
393
|
for (let k = 0; k < batch.length; k++) {
|
|
357
394
|
const tc = batch[k];
|
|
358
|
-
const
|
|
359
|
-
|
|
395
|
+
const outcome = await started[k];
|
|
396
|
+
const output = outcome.output;
|
|
397
|
+
hooks.onToolResult?.(tc, output, null, null, 1); // 并行工具无 diff
|
|
360
398
|
// Thrashing:history 里附 hint(UI 已用干净 output 渲染,避免屏幕噪声)
|
|
361
399
|
const hint = recordAndHint(tc.name, tc.arguments);
|
|
362
|
-
pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler);
|
|
363
|
-
}
|
|
364
|
-
hooks.onToolDone?.();
|
|
365
|
-
i = j;
|
|
366
|
-
}
|
|
367
|
-
else if (calls[i].name === 'task') {
|
|
368
|
-
// task 并发组:连续的 task 调用成组并发(子 agent 并行跑,各自独立 history)。
|
|
369
|
-
// 一次性启动全部(executeTool 即 spawnAgent,子 agent 开始跑),再并发 await + 渲染。
|
|
370
|
-
// task 是长任务,并发 fan-out 总耗时 ≈ 最慢一个子 agent。
|
|
371
|
-
// task 与 mutation/run_command 之间串行屏障(task 子 agent 可能有文件改动,不能和 write_file 乱序)。
|
|
372
|
-
// 渲染:先批量打印所有 ● 头 + 启 spinner(让用户看到多个 task 同时在跑),再逐个 await 出结果。
|
|
373
|
-
// (若像只读组那样「header → await → result」串行,长 task 的第二个 header 要等第一个跑完才出现,
|
|
374
|
-
// 视觉上只有一个在跑——与并发事实不符。)
|
|
375
|
-
//
|
|
376
|
-
// plan 模式防御 backstop(与单步串行分支同语义):schema 已剔除 task,正常不会进这里;
|
|
377
|
-
// 防后端幻觉调用——不执行(绝不派生子 agent,子 agent 可能有 mutation,违反只读),直接返错回灌。
|
|
378
|
-
if (getAgentMode() === 'plan') {
|
|
379
|
-
const tc = calls[i];
|
|
380
|
-
hooks.onToolHeader?.(tc);
|
|
381
|
-
const err = `错误:计划模式下禁用工具 ${tc.name}(仅读探查,不改动文件 / 不跑命令)`;
|
|
382
|
-
hooks.onToolResult?.(tc, err, null, null, 1);
|
|
383
|
-
// Thrashing:同上
|
|
384
|
-
const hint = recordAndHint(tc.name, tc.arguments);
|
|
385
|
-
pushToolResult(history, tc, hint ? `${err}${hint}` : err, relprune, lifecycle, scheduler);
|
|
386
|
-
i++;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
let j = i;
|
|
390
|
-
while (j < calls.length && calls[j].name === 'task')
|
|
391
|
-
j++;
|
|
392
|
-
const batch = calls.slice(i, j);
|
|
393
|
-
// 权限预检查:逐个 task 弹确认面板(在启动子 agent 之前,体验:先问再执行)。
|
|
394
|
-
// 拒绝的 task 直接跳过,不启动子 agent;放行的收集到 allowedBatch。
|
|
395
|
-
const allowedBatch = [];
|
|
396
|
-
for (const tc of batch) {
|
|
397
|
-
const parsed = parseArgs(tc.arguments);
|
|
398
|
-
const tool = tools.find((t) => t.name === tc.name);
|
|
399
|
-
if (tool) {
|
|
400
|
-
const perm = await checkPermission(tool, parsed ?? {}, signal);
|
|
401
|
-
if (perm === 'deny') {
|
|
402
|
-
hooks.onToolHeader?.(tc);
|
|
403
|
-
const err = `错误:用户拒绝了工具 ${tc.name} 的执行。`;
|
|
404
|
-
hooks.onToolResult?.(tc, err, null, null, 1);
|
|
405
|
-
const hint = recordAndHint(tc.name, tc.arguments);
|
|
406
|
-
pushToolResult(history, tc, hint ? `${err}${hint}` : err, relprune, lifecycle, scheduler);
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
allowedBatch.push(tc);
|
|
411
|
-
}
|
|
412
|
-
if (allowedBatch.length === 0) {
|
|
413
|
-
i = j;
|
|
414
|
-
continue; // 全部被拒绝,跳过执行
|
|
415
|
-
}
|
|
416
|
-
const started = allowedBatch.map((tc) => executeTool(tc.name, tc.arguments, signal, { dropContext }));
|
|
417
|
-
// 先批量打印所有头 + 启 spinner(多 task 并发,spinner 只显一个,但 ● 头都打出来)
|
|
418
|
-
for (const tc of allowedBatch) {
|
|
419
|
-
hooks.onToolHeader?.(tc);
|
|
420
|
-
}
|
|
421
|
-
hooks.onToolStart?.(allowedBatch[0].name); // spinner:多 task 共用一个「执行 task…」
|
|
422
|
-
// 逐个 await 出结果(按 tool_calls 原序,保 tool_call_id 配对);结果到即渲染 ↳
|
|
423
|
-
for (let k = 0; k < allowedBatch.length; k++) {
|
|
424
|
-
const tc = allowedBatch[k];
|
|
425
|
-
const output = await started[k];
|
|
426
|
-
hooks.onToolResult?.(tc, output, null, null, 1); // task 结果是摘要,无 diff
|
|
427
|
-
// Thrashing:同上
|
|
428
|
-
const hint = recordAndHint(tc.name, tc.arguments);
|
|
429
|
-
pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler);
|
|
400
|
+
pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
430
401
|
}
|
|
431
402
|
hooks.onToolDone?.();
|
|
432
403
|
i = j;
|
|
@@ -454,10 +425,10 @@ export async function runAgentCore(opts) {
|
|
|
454
425
|
const perm = await checkPermission(tool, parsed ?? {}, signal);
|
|
455
426
|
if (perm === 'deny') {
|
|
456
427
|
hooks.onToolHeader?.(tc);
|
|
457
|
-
const
|
|
458
|
-
hooks.onToolResult?.(tc,
|
|
428
|
+
const outcome = deniedOutcome(tc.name);
|
|
429
|
+
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
459
430
|
const hint = recordAndHint(tc.name, tc.arguments);
|
|
460
|
-
pushToolResult(history, tc, hint ? `${
|
|
431
|
+
pushToolResult(history, tc, hint ? `${outcome.output}${hint}` : outcome.output, relprune, lifecycle, scheduler, runtimeContextState, false);
|
|
461
432
|
i++;
|
|
462
433
|
continue;
|
|
463
434
|
}
|
|
@@ -468,14 +439,15 @@ export async function runAgentCore(opts) {
|
|
|
468
439
|
: null;
|
|
469
440
|
const { preWriteOld, editStartLine } = readDiffContext(tc, mutationParsed);
|
|
470
441
|
hooks.onToolStart?.(tc.name);
|
|
471
|
-
const
|
|
442
|
+
const outcome = await executeToolOutcome(tc.name, tc.arguments, signal, { dropContext });
|
|
443
|
+
const output = outcome.output;
|
|
472
444
|
hooks.onToolDone?.();
|
|
473
445
|
hooks.onToolResult?.(tc, output, mutationParsed, preWriteOld, editStartLine);
|
|
474
446
|
// Thrashing:同上(history 附 hint,UI 干净)
|
|
475
447
|
const hint = recordAndHint(tc.name, tc.arguments);
|
|
476
|
-
pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler);
|
|
448
|
+
pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
477
449
|
// 只有成功 mutation 才会使旧 read 失效;pruner 与 lifecycle 独立启停。
|
|
478
|
-
if (isMutationTool(tc.name) &&
|
|
450
|
+
if (isMutationTool(tc.name) && outcome.status === 'success') {
|
|
479
451
|
const mp = mutationParsed?.path;
|
|
480
452
|
if (typeof mp === 'string' && mp) {
|
|
481
453
|
relprune?.observeMutation(history, mp);
|
|
@@ -509,18 +481,100 @@ export async function runAgentCore(opts) {
|
|
|
509
481
|
});
|
|
510
482
|
continue; // 带着提示再调一次 LLM
|
|
511
483
|
}
|
|
512
|
-
//
|
|
484
|
+
// 没有工具调用:候选正文已流式打印。若本轮有新的代码变更,先通过框架验证门;
|
|
485
|
+
// failed 作为 system observation 风格的 user 消息回灌,不能伪造无配对的 tool 消息。
|
|
513
486
|
if (!gotText)
|
|
514
487
|
hooks.onNoReply?.();
|
|
515
|
-
|
|
488
|
+
const candidate = { role: 'assistant', content: result.content };
|
|
489
|
+
const mutationBeforeValidation = getCurrentTurnMutationState();
|
|
490
|
+
const shouldValidate = opts.autoValidate === true &&
|
|
491
|
+
getAgentMode() !== 'plan' &&
|
|
492
|
+
mutationBeforeValidation.version > validatedMutationVersion;
|
|
493
|
+
if (shouldValidate) {
|
|
494
|
+
history.push(candidate);
|
|
495
|
+
try {
|
|
496
|
+
latestValidation = await validator(signal, {
|
|
497
|
+
onCommandStart: (command) => hooks.onValidationStart?.(command),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
const mutation = getCurrentTurnMutationState();
|
|
502
|
+
latestValidation = {
|
|
503
|
+
status: signal?.aborted ? 'aborted' : 'failed',
|
|
504
|
+
output: `Automatic validation failed to run: ${error instanceof Error ? error.message : String(error)}`,
|
|
505
|
+
durationMs: 0,
|
|
506
|
+
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
507
|
+
mutationVersion: mutation.version,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
hooks.onValidationResult?.(latestValidation);
|
|
511
|
+
validatedMutationVersion = latestValidation.mutationVersion;
|
|
512
|
+
if (signal?.aborted || latestValidation.status === 'aborted') {
|
|
513
|
+
abortRestore();
|
|
514
|
+
traceStatus = 'aborted';
|
|
515
|
+
const mutation = getCurrentTurnMutationState();
|
|
516
|
+
return {
|
|
517
|
+
completed: false,
|
|
518
|
+
finalText: null,
|
|
519
|
+
validation: latestValidation,
|
|
520
|
+
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (latestValidation.status === 'failed') {
|
|
524
|
+
history.push({
|
|
525
|
+
role: 'user',
|
|
526
|
+
content: '[System observation: automatic validation failed]\n' +
|
|
527
|
+
`Command: ${latestValidation.command ?? '(internal verifier)'}\n` +
|
|
528
|
+
`${latestValidation.output}\n\n` +
|
|
529
|
+
'Fix the reported problem, then finish the task. Do not claim success until validation passes.',
|
|
530
|
+
});
|
|
531
|
+
// 验证及其失败观察均已完整落入 history;下一步中断时可安全保留。
|
|
532
|
+
savedHistory = history.slice();
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
history.push(candidate);
|
|
538
|
+
}
|
|
539
|
+
const finalMutation = getCurrentTurnMutationState();
|
|
516
540
|
done = true;
|
|
517
|
-
|
|
541
|
+
traceStatus = 'completed';
|
|
542
|
+
return {
|
|
543
|
+
completed: true,
|
|
544
|
+
finalText: result.content,
|
|
545
|
+
usage: turnUsage,
|
|
546
|
+
validation: latestValidation,
|
|
547
|
+
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
548
|
+
};
|
|
518
549
|
}
|
|
519
550
|
hooks.onMaxSteps?.();
|
|
520
551
|
done = true;
|
|
521
|
-
|
|
552
|
+
traceStatus = 'max_steps';
|
|
553
|
+
const finalMutation = getCurrentTurnMutationState();
|
|
554
|
+
return {
|
|
555
|
+
completed: true,
|
|
556
|
+
finalText: null,
|
|
557
|
+
usage: turnUsage,
|
|
558
|
+
validation: latestValidation,
|
|
559
|
+
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
560
|
+
};
|
|
522
561
|
}
|
|
523
562
|
finally {
|
|
563
|
+
const finalMutation = getCurrentTurnMutationState();
|
|
564
|
+
try {
|
|
565
|
+
opts.onTrace?.({
|
|
566
|
+
ts: new Date().toISOString(),
|
|
567
|
+
status: traceStatus,
|
|
568
|
+
durationMs: Date.now() - t0,
|
|
569
|
+
toolCalls: toolCallCount,
|
|
570
|
+
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
571
|
+
usage: turnUsage,
|
|
572
|
+
validation: latestValidation,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
// Trace is best-effort and must not change the turn result.
|
|
577
|
+
}
|
|
524
578
|
// 跑完(正常 / 达上限)在回复末尾打耗时摘要行;中断 done=false 不打。
|
|
525
579
|
if (done) {
|
|
526
580
|
hooks.onDone?.(Date.now() - t0, turnUsage);
|
|
@@ -528,4 +582,4 @@ export async function runAgentCore(opts) {
|
|
|
528
582
|
}
|
|
529
583
|
}
|
|
530
584
|
// ── 导出共享辅助(主 agent 的 TUI hooks 实现要用)──────────────────────────
|
|
531
|
-
export { parseArgs, readDiffContext, isMutationTool,
|
|
585
|
+
export { parseArgs, readDiffContext, isMutationTool, isParallelTool };
|
package/dist/agent/index.js
CHANGED
|
@@ -14,6 +14,7 @@ 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';
|
|
17
|
+
import { appendCurrentSessionTrace } from '../session/index.js';
|
|
17
18
|
/** 当前 turn 的 batch id(runAgent 内闭包变量;一条 turn 一轮 tool batch 结束即清空)。 */
|
|
18
19
|
let currentBatchId = null;
|
|
19
20
|
/** 取 userInput 的首行:字符串直接 split;多模态 parts 找首个 text part 再 split。 */
|
|
@@ -187,6 +188,23 @@ onContextUpdate) {
|
|
|
187
188
|
flushToolBatch();
|
|
188
189
|
layout.contentWrite(`${ui.dim}${t('agent.aborted')}${ui.reset}\n`);
|
|
189
190
|
},
|
|
191
|
+
onValidationStart: (command) => {
|
|
192
|
+
flushToolBatch();
|
|
193
|
+
spinner.start(t('agent.validating', { command }));
|
|
194
|
+
},
|
|
195
|
+
onValidationResult: (validation) => {
|
|
196
|
+
spinner.stop();
|
|
197
|
+
const color = validation.status === 'passed'
|
|
198
|
+
? ui.green
|
|
199
|
+
: validation.status === 'failed'
|
|
200
|
+
? ui.red
|
|
201
|
+
: ui.yellow;
|
|
202
|
+
const command = validation.command ?? t('agent.validationNoCommand');
|
|
203
|
+
const detail = validation.status === 'skipped' && validation.skipReason
|
|
204
|
+
? `${validation.status}: ${validation.skipReason}`
|
|
205
|
+
: validation.status;
|
|
206
|
+
layout.contentWrite(` ${color}●${ui.reset} ${t('agent.validationResult', { command, status: detail })}\n`);
|
|
207
|
+
},
|
|
190
208
|
onDone: (elapsedMs, usage) => {
|
|
191
209
|
flushToolBatch();
|
|
192
210
|
const tok = formatTurnTokens(usage);
|
|
@@ -210,6 +228,8 @@ onContextUpdate) {
|
|
|
210
228
|
signal,
|
|
211
229
|
onContextUpdate,
|
|
212
230
|
hooks: combinedHooks,
|
|
231
|
+
autoValidate: config.autoValidate,
|
|
232
|
+
onTrace: appendCurrentSessionTrace,
|
|
213
233
|
});
|
|
214
234
|
}
|
|
215
235
|
finally {
|
package/dist/agent/spawn.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// - 中断透传:opts.signal(主 agent 的 abort signal)透传给 runAgentCore → chat/executeTool,
|
|
13
13
|
// 主 Ctrl+C 树杀子 agent(chat 流式 abort + run_command/web_fetch 即时取消)。
|
|
14
14
|
import { chatTools } from '../llm/index.js';
|
|
15
|
-
import { config, isMemoryEnabled } from '../config/index.js';
|
|
15
|
+
import { config, isMemoryEnabled, isSubAgentEnabled } from '../config/index.js';
|
|
16
16
|
import { effectiveSystemPrompt } from '../skills/index.js';
|
|
17
17
|
import { buildMemorySection, buildMemoryIndexSection } from '../memory/index.js';
|
|
18
18
|
import { ui } from '../ui/theme.js';
|
|
@@ -40,6 +40,13 @@ You are a sub-agent spawned by the main agent to handle an isolated sub-task. Yo
|
|
|
40
40
|
* 子 agent 跑在主 signal 下,主 abort 即子 abort;子 agent 的 abortRestore 还原子 history + 模式。
|
|
41
41
|
*/
|
|
42
42
|
export async function spawnAgent(opts) {
|
|
43
|
+
if (!isSubAgentEnabled()) {
|
|
44
|
+
return {
|
|
45
|
+
summary: null,
|
|
46
|
+
completed: false,
|
|
47
|
+
transcript: 'Sub-agent execution is disabled. Enable it with /subagent on.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
43
50
|
const maxSteps = opts.maxSteps ?? config.subAgentMaxSteps ?? 50;
|
|
44
51
|
// 构造子 agent 系统提示:复用主 agent 组装链 + 子 agent 角色后缀 + 自定义后缀。
|
|
45
52
|
// config.systemPrompt 是 getter(每次访问现拼 buildBasePrompt,反映 isMemoryEnabled),
|
|
@@ -124,6 +131,7 @@ export async function spawnAgent(opts) {
|
|
|
124
131
|
maxSteps,
|
|
125
132
|
toolsOverride,
|
|
126
133
|
contextState: localContextState,
|
|
134
|
+
autoValidate: false, // 子 Agent 共享主轮工作区,由主 Agent 收尾统一验证
|
|
127
135
|
});
|
|
128
136
|
return {
|
|
129
137
|
summary: result.finalText,
|
package/dist/config/index.js
CHANGED
|
@@ -358,6 +358,7 @@ export const config = {
|
|
|
358
358
|
compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
|
|
359
359
|
includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
|
|
360
360
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
361
|
+
autoValidate: process.env.MOCODE_AUTO_VALIDATE !== 'false',
|
|
361
362
|
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
|
|
362
363
|
contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE !== 'false',
|
|
363
364
|
contextLifecycle: process.env.MOCODE_LIFECYCLE !== 'false',
|
|
@@ -366,6 +367,7 @@ export const config = {
|
|
|
366
367
|
memoryEnabled: process.env.MEMORY_ENABLED === 'true',
|
|
367
368
|
reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
|
|
368
369
|
maxSteps: Number(process.env.MAX_STEPS) || 200,
|
|
370
|
+
subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
|
|
369
371
|
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || 50,
|
|
370
372
|
sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
|
|
371
373
|
searchApiKey: process.env.ANYSEARCH_API_KEY,
|
|
@@ -379,6 +381,7 @@ export const config = {
|
|
|
379
381
|
llmKeysFromShell,
|
|
380
382
|
projectSnapshotEnabled: process.env.MOCODE_PROJECT_SNAPSHOT !== 'false',
|
|
381
383
|
permissionEnabled: process.env.MOCODE_PERMISSION !== 'false',
|
|
384
|
+
permissionNonInteractiveAllow: process.env.MOCODE_PERMISSION_NON_INTERACTIVE_ALLOW === 'true',
|
|
382
385
|
projectSkillEnabled: process.env.MOCODE_PROJECT_SKILL === 'true',
|
|
383
386
|
};
|
|
384
387
|
/**
|
|
@@ -407,6 +410,15 @@ export function updateModelConfig(opts) {
|
|
|
407
410
|
process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
|
|
408
411
|
}
|
|
409
412
|
}
|
|
413
|
+
/** 子 Agent 总开关;默认 false,关闭时 task 不进入模型工具表。 */
|
|
414
|
+
export function isSubAgentEnabled() {
|
|
415
|
+
return config.subAgentEnabled;
|
|
416
|
+
}
|
|
417
|
+
/** 运行时切换子 Agent;工具 schema 刷新与持久化由 REPL 调用方完成。 */
|
|
418
|
+
export function updateSubAgentConfig(enabled) {
|
|
419
|
+
config.subAgentEnabled = enabled;
|
|
420
|
+
process.env.MOCODE_SUBAGENT_ENABLED = enabled ? 'true' : 'false';
|
|
421
|
+
}
|
|
410
422
|
/**
|
|
411
423
|
* 记忆子系统总开关:单一来源。/memory_switch、/memory_status、buildSystemPrompt、
|
|
412
424
|
* tools/builtins/index.ts、tools/constants.ts 的 plan-mode 列表都从这里查。
|