mocode-ai 1.2.5 → 1.2.7
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 +6 -6
- package/README.zh-CN.md +4 -4
- package/dist/agent/core.js +77 -37
- package/dist/agent/index.js +70 -34
- package/dist/agent/spawn.js +4 -0
- package/dist/commands/skill.js +230 -0
- package/dist/config/index.js +138 -12
- package/dist/config/presets.js +29 -7
- package/dist/context/artifacts.js +20 -0
- package/dist/context/budget.js +15 -6
- package/dist/context/encoders/table.js +1 -1
- package/dist/context/index.js +1 -1
- package/dist/host/stdio.js +10 -1
- package/dist/i18n/index.js +4 -4
- package/dist/llm/index.js +22 -3
- package/dist/llm/providers/anthropic.js +370 -0
- package/dist/memory/discover.js +8 -8
- package/dist/memory/index.js +3 -2
- package/dist/repl/index.js +94 -49
- package/dist/session/compact.js +24 -2
- package/dist/session/notes.js +233 -0
- package/dist/session/persist.js +2 -2
- package/dist/session/scheduler.js +4 -4
- package/dist/skills/runner.js +0 -1
- package/dist/skills/skill-eval.js +345 -0
- package/dist/skills/skill-improve.js +221 -0
- package/dist/skills/stats.js +102 -0
- package/dist/tools/builtins/index.js +4 -0
- package/dist/tools/builtins/note-append.js +103 -0
- package/dist/tools/registry.js +18 -5
- package/dist/ui/diff.js +1 -1
- package/dist/ui/layout.js +12 -1
- package/dist/ui/render.js +7 -1
- package/dist/verification/prompt.js +55 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
|
|
|
79
79
|
- **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.
|
|
80
80
|
- **Pressure-driven context compression** — Normal history keeps full tool evidence. At 80% occupancy, one scheduler event runs all enabled cleanup and always follows with a history summary. `/context` shows live usage and `/compact` remains an explicit manual override.
|
|
81
81
|
- **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.
|
|
82
|
-
- **Project context (`
|
|
82
|
+
- **Project context (`AGENTS.md`)** — A single project-level memory file at `AGENTS.md` captures both static facts (project description, commands, module list, directory tree) and human/AI-written insights (conventions, architectural decisions, pitfalls). Generate it once with `/init`, then keep it up to date by hand or by asking the agent to refresh it. Loaded automatically into the system prompt on every turn.
|
|
83
83
|
- **Session notepad (notes.md)** — For complex multi-step tasks (≥3 file changes / ≥5 tool calls), the agent maintains a working notepad at `.mocode/sessions/<sessionId>/notes.md` (file-based, survives context compression). It records the execution plan with the dedicated `plan_update` tool — a three-state step machine (`pending`/`in_progress`/`completed`, at most one `in_progress`) that auto-settles to `## Done:` when finished. The active plan is re-injected into the system prompt after compaction and re-synced into context whenever notes.md changes, and a gentle reminder nudges the agent if it goes several tool-steps without updating the plan. A live progress chip in the TUI status bar shows `plan: [title] (3/7) ▸ [current step]`.
|
|
84
84
|
- **Interruptible and reversible** — Ctrl+C interrupts the current turn at any time (kills child processes recursively, rolls history back to before the turn started, leaves no half-finished tool calls). `/rollback` restores file changes from per-turn snapshots, with a per-file keep/undo choice — no git dependency required.
|
|
85
85
|
- **Sandbox protection** — File reads/writes go through a sandbox that blocks out-of-bounds paths (`../../`, absolute paths outside the root, symlink escapes, etc.), so the agent never touches files outside your working directory.
|
|
@@ -96,7 +96,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
|
|
|
96
96
|
## Documentation
|
|
97
97
|
|
|
98
98
|
- [中文使用指南](./docs/usage.md) — 菜单式快速上手、命令速查、模式、会话、项目上下文与排障。
|
|
99
|
-
- [Project context](./docs/usage.md#项目上下文) — `
|
|
99
|
+
- [Project context](./docs/usage.md#项目上下文) — `AGENTS.md` and Skills.
|
|
100
100
|
|
|
101
101
|
## Installation
|
|
102
102
|
|
|
@@ -218,7 +218,7 @@ The agent operates in **the working directory it was launched from** — to have
|
|
|
218
218
|
| `memory_update` | Edit a memory in place (id unchanged; correct stale facts / update summary / toggle pin) |
|
|
219
219
|
| `memory_forget` | Forget a memory: archived by default (recoverable), `mode=delete` for a hard delete (pinned memories can't be deleted) |
|
|
220
220
|
|
|
221
|
-
The five `memory_*` tools are gated on `MEMORY_ENABLED=true` at startup; toggle at runtime with `/memory_switch` (REPL restart required, by design — see Skills section for the difference between Tier-1 `
|
|
221
|
+
The five `memory_*` tools are gated on `MEMORY_ENABLED=true` at startup; toggle at runtime with `/memory_switch` (REPL restart required, by design — see Skills section for the difference between Tier-1 `AGENTS.md` and Tier-2 memory).
|
|
222
222
|
|
|
223
223
|
The four frontend tools — `browser`, `dev_server`, `screenshot`, `view_image` — are **off by default** (they depend on the Playwright binary, spawn long-lived processes, or capture the desktop). Enable the whole cluster at runtime with `/fe on`; the model only sees them once enabled. Toggle with `/fe on|off|status`.
|
|
224
224
|
|
|
@@ -254,7 +254,7 @@ dev_server stop id=srv-xxxx
|
|
|
254
254
|
| `/memory_switch` | Toggle Tier-2 memory on/off (REPL restart required — by design) |
|
|
255
255
|
| `/reflect` | Manually trigger a background memory reflection pass |
|
|
256
256
|
| `/model` | Configure the LLM (baseURL / apiKey / model / context window), applied immediately + persisted |
|
|
257
|
-
| `/init` | Scan the project and generate `
|
|
257
|
+
| `/init` | Scan the project and generate `AGENTS.md` project memory (dispatched to the agent) |
|
|
258
258
|
| `/theme` | Switch color theme (↑↓ · Enter, or `/theme <name>` directly) |
|
|
259
259
|
| `/plan` | Switch to plan mode (read-only exploration + plan output, approve to switch to auto) |
|
|
260
260
|
| `/auto` | Switch back to auto mode (full toolset execution) |
|
|
@@ -293,11 +293,11 @@ A skill's `description` is injected into the system prompt (progressive disclosu
|
|
|
293
293
|
|
|
294
294
|
The system prompt provides lightweight guidance rather than a framework gate: inspect only what matters, make focused changes, avoid repeated stale reads, and report uncertainty honestly. The agent decides whether validation is useful for the task and chooses the scope itself. Broad test/build suites are not run by default, and lack of validation never blocks completion or triggers an extra model turn.
|
|
295
295
|
|
|
296
|
-
## Project memory (
|
|
296
|
+
## Project memory (AGENTS.md)
|
|
297
297
|
|
|
298
298
|
MoCode has a **two-tier memory** model distinct from skills:
|
|
299
299
|
|
|
300
|
-
- **Tier-1 — `
|
|
300
|
+
- **Tier-1 — `AGENTS.md` (auto-loaded every session):** Markdown project memory that gets concatenated into the system prompt on every turn. Discovery walks `~/.mocode/AGENTS.md` → every `AGENTS.md` from the cwd up to the filesystem root (far→near, near wins). On overflow the body is truncated with a marker pointing back at the files. Generate or refresh one with `/init`, or write it by hand — it's plain Markdown, no schema. `AGENTS.md` is also where the agent itself persists "next-session facts" it deduces (architecture, conventions, pitfalls).
|
|
301
301
|
- **Tier-2 — `memory_*` tool library (agent-driven, opt-in):** Discrete tagged records (`decision` / `fact` / `pitfall` / `reference` / `feedback`) with recall-count-based decay (30-day → archived; 90-day → GC). The agent saves / searches / updates / forgets via tools; titles go in the system-prompt index (≤50), bodies fetched on demand via `memory_search`. Off by default; toggle with `MEMORY_ENABLED=true` at startup or `/memory_switch` (REPL restart required).
|
|
302
302
|
|
|
303
303
|
## Type checking
|
package/README.zh-CN.md
CHANGED
|
@@ -95,7 +95,7 @@ mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
|
|
|
95
95
|
## 使用文档
|
|
96
96
|
|
|
97
97
|
- [菜单式使用指南](./docs/usage.md) — 快速上手、命令速查、模式、会话、项目上下文与排障。
|
|
98
|
-
- [项目上下文:`
|
|
98
|
+
- [项目上下文:`AGENTS.md` 与 Skills](./docs/usage.md#项目上下文)
|
|
99
99
|
|
|
100
100
|
## 安装
|
|
101
101
|
|
|
@@ -257,7 +257,7 @@ dev_server stop id=srv-xxxx
|
|
|
257
257
|
| `/memory_switch` | 切换 Tier-2 记忆开关(需重启 REPL,刻意为之) |
|
|
258
258
|
| `/reflect` | 手动触发一次后台记忆反思 pass |
|
|
259
259
|
| `/model` | 配置大模型(baseURL / apiKey / model / 上下文窗口),即时生效 + 持久化 |
|
|
260
|
-
| `/init` | 扫描项目生成 `
|
|
260
|
+
| `/init` | 扫描项目生成 `AGENTS.md` 项目记忆(发给 agent 执行) |
|
|
261
261
|
| `/theme` | 切换颜色主题(↑↓ · Enter,或 `/theme <name>` 直切) |
|
|
262
262
|
| `/plan` | 切到 plan 模式(只读探查 + 产出计划,审批后切 auto 执行) |
|
|
263
263
|
| `/auto` | 切回 auto 模式(全量工具执行) |
|
|
@@ -296,11 +296,11 @@ skill 的 `description` 注入系统提示(渐进式披露第①层),模型只
|
|
|
296
296
|
|
|
297
297
|
system prompt 提供轻量建议而不是框架硬门:只检查支持下一步决策的内容,做最小完整改动,避免重复读取,并诚实说明不确定性。agent 自主决定是否需要验证以及验证范围;框架不会因为未验证阻止完成或追加模型轮次。
|
|
298
298
|
|
|
299
|
-
## 项目记忆(
|
|
299
|
+
## 项目记忆(AGENTS.md)
|
|
300
300
|
|
|
301
301
|
mocode 的**双层记忆**模型,跟 Skills 是两件事:
|
|
302
302
|
|
|
303
|
-
- **Tier-1 — `
|
|
303
|
+
- **Tier-1 — `AGENTS.md`(每轮自动加载):** Markdown 项目记忆,每轮拼进 system prompt。发现路径:`~/.mocode/AGENTS.md` → 从 cwd 往上逐级 `AGENTS.md`(远→近拼接,近的覆盖更突出);超长截断并标注原始文件。运行 `/init` 生成或刷新,纯 Markdown,可手写,无 schema。agent 自己推得的「下次要记住的事实」(架构/约定/坑位)也写在这里。
|
|
304
304
|
- **Tier-2 — `memory_*` 工具库(agent 主导,需启用):** 离散带标签条目(`decision` / `fact` / `pitfall` / `reference` / `feedback`),按召回计数衰减(30 天 → archived,90 天 → 硬删 GC)。agent 用工具存 / 搜 / 改 / 删;索引(标题)进系统提示(≤50 条),正文按需 `memory_search` 取。默认关,启动 `MEMORY_ENABLED=true` 或 REPL 内 `/memory_switch`(需重启 REPL,刻意为之)。
|
|
305
305
|
|
|
306
306
|
## 类型检查
|
package/dist/agent/core.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// spawn.ts 的 spawnAgent = runAgentCore + 静默 hooks(子 agent)。
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { getNotesMtime } from '../session/notes.js';
|
|
9
|
-
import { chat, estimatePromptTokens, planChatTools, chatTools, } from '../llm/index.js';
|
|
10
|
-
import { executeToolOutcome, getToolCapabilities, isFileMutationTool,
|
|
9
|
+
import { chat, estimatePromptTokens, estimateTokens, planChatTools, chatTools, } from '../llm/index.js';
|
|
10
|
+
import { executeToolOutcome, findTool, getToolCapabilities, isFileMutationTool, } from '../tools/registry.js';
|
|
11
11
|
import { checkPermission } from '../permissions/index.js';
|
|
12
12
|
import { validateToolArguments } from '../tools/validation.js';
|
|
13
13
|
import { getPlanDisabledTools, getRuntimeDisabledTools } from '../tools/constants.js';
|
|
@@ -15,10 +15,10 @@ import { getAgentMode, setAgentMode } from './mode.js';
|
|
|
15
15
|
import { maybeCompact, contextState, createTraceEvent, summarizeToolArguments, safeProviderId, } from '../session/index.js';
|
|
16
16
|
import { capToolResultForHistory } from '../session/compact.js';
|
|
17
17
|
import { createBudgetScheduler } from '../session/scheduler.js';
|
|
18
|
-
import { recordArtifact, invalidateArtifacts, rehydrateArtifacts, } from '../context/index.js';
|
|
18
|
+
import { recordArtifact, invalidateArtifacts, rehydrateArtifacts, knownEditTargets, } from '../context/index.js';
|
|
19
19
|
import { createRelevancePruner } from '../context/relevance.js';
|
|
20
20
|
import { isToolResultSuccess } from '../context/utils.js';
|
|
21
|
-
import { config, extractActivePlanSection,
|
|
21
|
+
import { config, getActiveModel, extractActivePlanSection, buildSessionStateReminder } from '../config/index.js';
|
|
22
22
|
import { t } from '../i18n/index.js';
|
|
23
23
|
import { jailResolve } from '../sandbox/index.js';
|
|
24
24
|
import { createLifecycleEngine } from '../context/lifecycle.js';
|
|
@@ -40,15 +40,36 @@ function parseArgs(raw) {
|
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/** 需要「已知编辑目标」恢复提示的文件编辑工具;其余工具的参数报错不注入该提示。 */
|
|
44
|
+
const EDIT_HINT_TOOLS = new Set(['edit_file', 'write_file']);
|
|
45
|
+
/** 文件编辑工具参数校验失败时的恢复提示:把系统已知仍新鲜的「最近 read_file 的
|
|
46
|
+
* path + hash」直接递给模型照抄,替代其在长上下文里凭记忆复述。
|
|
47
|
+
* 只展示事实、不替模型填值;没有候选(从未 read_file / 全部已失效)返 undefined。 */
|
|
48
|
+
function argumentErrorHint(name, state) {
|
|
49
|
+
if (!EDIT_HINT_TOOLS.has(name))
|
|
50
|
+
return undefined;
|
|
51
|
+
const targets = knownEditTargets(state);
|
|
52
|
+
if (targets.length === 0)
|
|
53
|
+
return undefined;
|
|
54
|
+
const lines = targets.map((target) => ` path=${target.path} expected_hash=${target.hash}`).join('\n');
|
|
55
|
+
return ('系统已知最近 read_file 且尚未被修改的文件(直接复制下面的 path / expected_hash,勿凭记忆复述):\n' +
|
|
56
|
+
`${lines}\n如目标文件不在其中,先 read_file 该文件再发起编辑。`);
|
|
57
|
+
}
|
|
58
|
+
/** 判定 assistant content 是否只是 "Tool results:" 这类工具结果前缀噪声。
|
|
59
|
+
* 部分模型(如 Claude)会在 tool_calls 前输出此种无意义过渡文本,写入 history
|
|
60
|
+
* 会污染后续轮次上下文并在 TUI 上泄露为孤立行。 */
|
|
61
|
+
function isToolResultsNoise(content) {
|
|
62
|
+
return /^(?:\s*Tool results:\s*)+$/i.test(content.trim());
|
|
63
|
+
}
|
|
43
64
|
/** 只有显式声明 parallel 且无需权限确认的工具才进入普通并发组。 */
|
|
44
65
|
function isParallelTool(name) {
|
|
45
|
-
const tool =
|
|
66
|
+
const tool = findTool(name);
|
|
46
67
|
return !!tool && (tool.risk ?? 'safe') === 'safe' &&
|
|
47
68
|
getToolCapabilities(tool).concurrency === 'parallel';
|
|
48
69
|
}
|
|
49
70
|
/** resource-locked 工具先顺序完成权限预检,再依赖 canonical resource lock 并发执行。 */
|
|
50
71
|
function isResourceLockedTool(name) {
|
|
51
|
-
const tool =
|
|
72
|
+
const tool = findTool(name);
|
|
52
73
|
return !!tool && getToolCapabilities(tool).concurrency === 'resource-locked';
|
|
53
74
|
}
|
|
54
75
|
function isResourceLockedCall(call) {
|
|
@@ -203,6 +224,7 @@ export async function runAgentCore(opts) {
|
|
|
203
224
|
completionTokens: turnUsage.completionTokens + u.completionTokens,
|
|
204
225
|
totalTokens: turnUsage.totalTokens + u.totalTokens,
|
|
205
226
|
cachedTokens: turnUsage.cachedTokens + u.cachedTokens,
|
|
227
|
+
cacheCreationTokens: (turnUsage.cacheCreationTokens ?? 0) + (u.cacheCreationTokens ?? 0),
|
|
206
228
|
reasoningTokens: turnUsage.reasoningTokens + u.reasoningTokens,
|
|
207
229
|
}
|
|
208
230
|
: u;
|
|
@@ -283,10 +305,16 @@ export async function runAgentCore(opts) {
|
|
|
283
305
|
const activeTools = opts.toolsOverride
|
|
284
306
|
?? (getAgentMode() === 'plan' ? planChatTools : chatTools);
|
|
285
307
|
const requestBaseURL = config.baseURL;
|
|
286
|
-
const requestModel =
|
|
308
|
+
const requestModel = getActiveModel();
|
|
287
309
|
const storedCalibration = getTokenCalibration(requestBaseURL, requestModel, activeTools);
|
|
288
310
|
runtimeContextState.correction = storedCalibration.correction;
|
|
289
311
|
runtimeContextState.calibrationSamples = storedCalibration.samples;
|
|
312
|
+
// 会话状态(活跃 plan + 笔记正文)在调度器**之前**取一次:
|
|
313
|
+
// ① 它会被追加到本次请求末尾(见下方 ephemeralReminder),属于本步固定开销,
|
|
314
|
+
// 必须计入压力线——它不在 history 里,调度器只能由此入参看见(否则最多 5k
|
|
315
|
+
// 的笔记 + plan 段对 80% 触发线完全不可见,小窗口模型会压不住);
|
|
316
|
+
// ② 同一份字符串复用到下方 reminder,避免每步重复读 notes.md。
|
|
317
|
+
const sessionStateText = opts.suppressSessionState ? '' : buildSessionStateReminder();
|
|
290
318
|
// The scheduler is the only automatic path that may compress old evidence.
|
|
291
319
|
// Normal tool pushes and lifecycle tracking remain metadata-only.
|
|
292
320
|
// 步前:五区 Budget Scheduler 在当前完整 history 上决策;开关关闭时退化回 maybeCompact 路径。
|
|
@@ -294,7 +322,7 @@ export async function runAgentCore(opts) {
|
|
|
294
322
|
let historyRebuilt = false;
|
|
295
323
|
const compactStartedAt = Date.now();
|
|
296
324
|
if (scheduler) {
|
|
297
|
-
historyRebuilt = await scheduler.runStep(history, step, activeTools);
|
|
325
|
+
historyRebuilt = await scheduler.runStep(history, step, activeTools, sessionStateText ? estimateTokens(sessionStateText) : 0);
|
|
298
326
|
if (scheduler.lastRunLog?.compactHistoryCalled) {
|
|
299
327
|
emitTrace('compact', {
|
|
300
328
|
source: 'automatic',
|
|
@@ -326,8 +354,8 @@ export async function runAgentCore(opts) {
|
|
|
326
354
|
runtimeContextState.lifecycleStats = lifecycle.stats();
|
|
327
355
|
}
|
|
328
356
|
rehydrateArtifacts(runtimeContextState, history);
|
|
329
|
-
//
|
|
330
|
-
|
|
357
|
+
// 会话状态(活跃 plan + 笔记段)不再回写 history[0]:每步都会在 requestHistory
|
|
358
|
+
// 末尾注入最新副本(见下方 ephemeralReminder),compact 后自然恢复。
|
|
331
359
|
}
|
|
332
360
|
hooks.onStepStart?.(); // 主 agent:spinner.start('思考中')
|
|
333
361
|
mode = 'idle';
|
|
@@ -337,31 +365,29 @@ export async function runAgentCore(opts) {
|
|
|
337
365
|
const modelStartedAt = Date.now();
|
|
338
366
|
const provider = safeProviderId(requestBaseURL);
|
|
339
367
|
emitTrace('model_start', { model: requestModel, provider });
|
|
340
|
-
// 动态注入(
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
368
|
+
// 动态注入(prompt 缓存关键):所有随步/随文件变化的提示统一拼成**历史末尾**一条
|
|
369
|
+
// ephemeral system 消息,不再改写 history[0]。这样系统提示 + 已有对话逐字节稳定,
|
|
370
|
+
// 支持自动前缀缓存的后端(OpenAI / DeepSeek / GLM / Qwen)可从头命中,只有尾部这
|
|
371
|
+
// 一小条随内容变化;若改写 history[0],单次 plan_update 就会让 6-8k 的系统提示
|
|
372
|
+
// 在本轮后续每步全价重算。
|
|
373
|
+
// - 开场分析:仅主线(step===0 且 !suppressOpeningAnalysis)——用户发任务后 agent
|
|
374
|
+
// 的第一次模型调用。子代理经 spawn.ts 传 suppressOpeningAnalysis:true 排除。
|
|
375
|
+
// 放在尾部还有额外好处:step 0 与 step 1 的前缀不再因这段的出现/消失而错位。
|
|
376
|
+
// - historyRebuilt:compact 恢复步,要求重新锚定目标。
|
|
377
|
+
// - 会话状态:notes.md 的活跃 plan + 笔记正文(纯读,每步重取,始终最新)。
|
|
378
|
+
// .filter(Boolean) 保证空段不产生多余空行;三段全空时不追加任何消息(requestHistory === history)。
|
|
379
|
+
// 安全保证:只拼进 requestHistory(新建数组),绝不写回 history,故不会跨 step/跨 turn 残留。
|
|
380
|
+
const ephemeralReminder = [
|
|
347
381
|
(!opts.suppressOpeningAnalysis && step === 0)
|
|
348
382
|
? '## Opening analysis\nBegin your FIRST response of this turn with a brief analysis of the request and your planned approach (1-3 sentences, no filler), THEN start tool calls. This opening is the only place where pre-tool prose is expected; after it, work quietly with no narration between tool calls.'
|
|
349
383
|
: '',
|
|
350
384
|
historyRebuilt
|
|
351
385
|
? '## Post-compaction recovery\nContext was compacted before this request. Re-establish the current objective and unresolved work from retained evidence or the session note, avoid repeating completed investigation, and re-read exact file context before any dependent edit.'
|
|
352
386
|
: '',
|
|
387
|
+
sessionStateText, // 调度器之前已取(并计入压力线),此处复用同一份,不重复读文件
|
|
353
388
|
].filter(Boolean).join('\n\n');
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
&& systemMessage?.role === 'system'
|
|
357
|
-
&& typeof systemMessage.content === 'string'
|
|
358
|
-
? [
|
|
359
|
-
{
|
|
360
|
-
...systemMessage,
|
|
361
|
-
content: `${systemMessage.content}\n\n${dynamicSystemSuffix}`,
|
|
362
|
-
},
|
|
363
|
-
...history.slice(1),
|
|
364
|
-
]
|
|
389
|
+
const requestHistory = ephemeralReminder
|
|
390
|
+
? [...history, { role: 'system', content: ephemeralReminder }]
|
|
365
391
|
: history;
|
|
366
392
|
// 实时用量:当前步 prompt 估算(含校准系数)+ 流式累计 completion 估算,
|
|
367
393
|
// 叠上已完成步的实测 turnUsage,经 onLiveUsage 推给底栏实时 chip。
|
|
@@ -461,6 +487,14 @@ export async function runAgentCore(opts) {
|
|
|
461
487
|
onContextUpdate?.();
|
|
462
488
|
if (result.toolCalls.length > 0) {
|
|
463
489
|
toolCallCount += result.toolCalls.length;
|
|
490
|
+
// 若 content 只是 Claude 式 "Tool results:" 噪声,清空它:不补换行、不写入 history,
|
|
491
|
+
// 避免污染后续轮次上下文并在 TUI 泄露为孤立行。
|
|
492
|
+
if (result.content && isToolResultsNoise(result.content)) {
|
|
493
|
+
result.content = null;
|
|
494
|
+
mode = 'idle';
|
|
495
|
+
gotText = false;
|
|
496
|
+
lastChar = '';
|
|
497
|
+
}
|
|
464
498
|
// 流式正文末尾补换行(若 onToolCall 已补则 lastChar='\n',此处 no-op);防 ● 行黏在正文行尾
|
|
465
499
|
if (mode !== 'idle' && lastChar !== '\n')
|
|
466
500
|
hooks.onTextEnd?.();
|
|
@@ -602,7 +636,7 @@ export async function runAgentCore(opts) {
|
|
|
602
636
|
for (let k = 0; k < batch.length; k++) {
|
|
603
637
|
const tc = batch[k];
|
|
604
638
|
const parsed = parseArgs(tc.arguments);
|
|
605
|
-
const tool =
|
|
639
|
+
const tool = findTool(tc.name);
|
|
606
640
|
const argumentsValid = tool && parsed !== null
|
|
607
641
|
? validateToolArguments(tool, parsed).valid
|
|
608
642
|
: false;
|
|
@@ -635,14 +669,18 @@ export async function runAgentCore(opts) {
|
|
|
635
669
|
const firstAllowed = entries.find((entry) => !entry.denied);
|
|
636
670
|
if (firstAllowed)
|
|
637
671
|
hooks.onToolStart?.(firstAllowed.tc.name);
|
|
638
|
-
const started = entries.map((entry) =>
|
|
639
|
-
|
|
640
|
-
|
|
672
|
+
const started = entries.map((entry) => {
|
|
673
|
+
if (entry.denied)
|
|
674
|
+
return Promise.resolve(entry.denied);
|
|
675
|
+
const hint = argumentErrorHint(entry.tc.name, runtimeContextState);
|
|
676
|
+
return executeToolOutcome(entry.tc.name, entry.tc.arguments, signal, {
|
|
641
677
|
callId: entry.tc.id,
|
|
678
|
+
...(hint ? { argumentErrorHint: hint } : {}),
|
|
642
679
|
onLockAcquired: (lockedArgs) => {
|
|
643
680
|
entry.diff = readDiffContext(entry.tc, lockedArgs);
|
|
644
681
|
},
|
|
645
|
-
})
|
|
682
|
+
});
|
|
683
|
+
});
|
|
646
684
|
for (let k = 0; k < entries.length; k++) {
|
|
647
685
|
const entry = entries[k];
|
|
648
686
|
const outcome = await started[k];
|
|
@@ -693,7 +731,7 @@ export async function runAgentCore(opts) {
|
|
|
693
731
|
// 权限预检查:在渲染 ● 头之前弹确认面板(体验:先问再执行,而非执行完再问)。
|
|
694
732
|
// 拒绝时只渲染拒绝结果,不渲染执行头;放行则继续走 header → start → executeTool 流程。
|
|
695
733
|
const parsed = parseArgs(tc.arguments);
|
|
696
|
-
const tool =
|
|
734
|
+
const tool = findTool(tc.name);
|
|
697
735
|
const argumentsValid = tool && parsed !== null
|
|
698
736
|
? validateToolArguments(tool, parsed).valid
|
|
699
737
|
: false;
|
|
@@ -726,8 +764,10 @@ export async function runAgentCore(opts) {
|
|
|
726
764
|
: null;
|
|
727
765
|
let diff = readDiffContext(tc, mutationParsed);
|
|
728
766
|
hooks.onToolStart?.(tc.name);
|
|
767
|
+
const serialHint = argumentErrorHint(tc.name, runtimeContextState);
|
|
729
768
|
const outcome = await executeToolOutcome(tc.name, tc.arguments, signal, {
|
|
730
769
|
callId: tc.id,
|
|
770
|
+
...(serialHint ? { argumentErrorHint: serialHint } : {}),
|
|
731
771
|
onLockAcquired: (lockedArgs) => {
|
|
732
772
|
if (mutationParsed)
|
|
733
773
|
diff = readDiffContext(tc, lockedArgs);
|
|
@@ -763,12 +803,12 @@ export async function runAgentCore(opts) {
|
|
|
763
803
|
i++;
|
|
764
804
|
}
|
|
765
805
|
}
|
|
766
|
-
// A(
|
|
767
|
-
//
|
|
806
|
+
// A(计划触碰计数):本步若改动了 notes.md 则清零计数。会话状态本身无需在此重注入——
|
|
807
|
+
// 每步都会由 buildSessionStateReminder() 在 requestHistory 末尾重建最新副本(见上方注入点),
|
|
808
|
+
// 所以模型下一步看到的必然是当前勾选态。只保留计数,避免多余的 history 改写(prompt 缓存)。
|
|
768
809
|
// B(nag 提醒):连续 N 步有工具活动但没更新 plan,在当前步第一条 tool_result 前注入提醒。
|
|
769
810
|
const notesMtimeAfter = getNotesMtime();
|
|
770
811
|
if (notesMtimeAfter !== notesMtimeBefore) {
|
|
771
|
-
reinjectActivePlanIntoSystem(history);
|
|
772
812
|
stepsSincePlanTouch = 0;
|
|
773
813
|
}
|
|
774
814
|
else {
|
package/dist/agent/index.js
CHANGED
|
@@ -7,7 +7,6 @@ import { Spinner } from '../ui/spinner.js';
|
|
|
7
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
|
-
import * as content from '../ui/content.js';
|
|
11
10
|
import * as batch from '../ui/batch.js';
|
|
12
11
|
import { beginTurn } from '../rollback/index.js';
|
|
13
12
|
import { config } from '../config/index.js';
|
|
@@ -27,17 +26,10 @@ let subAgentGroupPendingSeparator = false;
|
|
|
27
26
|
/** 派生子 agent 的工具名。它的调用要独占一批:子 agent 的实时工具明细会挂到这一行下面,
|
|
28
27
|
* 并行派发多个子 agent 时,每个子 agent 才有自己可归属的摘要行。 */
|
|
29
28
|
const SUB_AGENT_TOOL = 'sub-agent';
|
|
30
|
-
/** 缓冲尾部是否已经是空白行(去掉 ANSI 后无可见字符)。用于避免 sub-agent 分隔空行叠成两行。
|
|
29
|
+
/** 缓冲尾部是否已经是空白行(去掉 ANSI 后无可见字符)。用于避免 sub-agent 分隔空行叠成两行。
|
|
30
|
+
* 实现委托 layout.isLastContentRowBlank(compact 等模块共用同一判断)。 */
|
|
31
31
|
function isLastContentRowBlank() {
|
|
32
|
-
|
|
33
|
-
// 永远空白,不能把它当作“已经有一条空行分隔”。
|
|
34
|
-
const committed = content.committedRows();
|
|
35
|
-
if (committed === 0)
|
|
36
|
-
return false;
|
|
37
|
-
const line = content.lineAt(committed - 1);
|
|
38
|
-
if (line === null)
|
|
39
|
-
return false;
|
|
40
|
-
return line.replace(/\x1b\[[0-9;]*m/g, '').trim().length === 0;
|
|
32
|
+
return layout.isLastContentRowBlank();
|
|
41
33
|
}
|
|
42
34
|
let turnFileChanges = [];
|
|
43
35
|
function lineDelta(oldText, newText) {
|
|
@@ -274,33 +266,73 @@ onContextUpdate) {
|
|
|
274
266
|
let textBoundaryNewlines = 0;
|
|
275
267
|
let hasPendingTextBoundary = false;
|
|
276
268
|
let toolBatchFollowsText = false;
|
|
269
|
+
// Claude 等模型在 tool_calls 前常输出 "Tool results:" 这类无意义叙述。
|
|
270
|
+
// 它违反 silent execution,会在正文区泄露成孤立行。这里先缓冲,若累积内容
|
|
271
|
+
// 只是该噪声则抑制;一旦后面跟了实质正文,再丢弃噪声前缀并 flush。
|
|
272
|
+
let pendingNarration = '';
|
|
273
|
+
let narrationIsNoise = false;
|
|
274
|
+
const TOOL_RESULTS_NOISE_RE = /^(?:\s*Tool results:\s*)+$/i;
|
|
275
|
+
function writeAssistantText(s) {
|
|
276
|
+
const inToolBlock = currentBatchId !== null || subAgentGroupId !== null || subAgentGroupPendingSeparator;
|
|
277
|
+
if (inToolBlock && s.trim().length === 0)
|
|
278
|
+
return;
|
|
279
|
+
const followsToolBatch = inToolBlock;
|
|
280
|
+
// batch 收尾已经统一留了一条空白行。部分后端会把下一段正文以 \n / \n\n
|
|
281
|
+
// 开头发来;去掉这些“边界换行”,避免与 UI 分隔叠成两条空白行。
|
|
282
|
+
const visible = followsToolBatch ? s.replace(/^(?:[ \t]*\r?\n)+/, '') : s;
|
|
283
|
+
// 正文是工具批次边界:只有“连续且中间没有正文”的工具调用才合并。
|
|
284
|
+
// 一旦模型开始解释阶段结果,立即收尾当前摘要;后续工具重新建立批次。
|
|
285
|
+
if (visible)
|
|
286
|
+
flushToolBatch();
|
|
287
|
+
spinner.stop(); // 任何正文 token 都停 spinner(首 token 停「思考中」;onToolCall 重启后若又来文本则停「生成中」)。未旋转时 stop 为 no-op。
|
|
288
|
+
layout.contentWriteMd(visible); // 正文走 markdown 渲染(代码块高亮 / 标题 / 列表 / 行内 …),见 ui/markdown.ts
|
|
289
|
+
if (visible) {
|
|
290
|
+
lastChar = visible[visible.length - 1];
|
|
291
|
+
if (visible.trim().length > 0) {
|
|
292
|
+
hasPendingTextBoundary = true;
|
|
293
|
+
textBoundaryNewlines = 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function trySuppressNoise(s) {
|
|
298
|
+
pendingNarration += s;
|
|
299
|
+
const stripped = pendingNarration.replace(/\s+/g, ' ').trim();
|
|
300
|
+
if (TOOL_RESULTS_NOISE_RE.test(stripped)) {
|
|
301
|
+
narrationIsNoise = true;
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
let visible = pendingNarration;
|
|
305
|
+
if (narrationIsNoise) {
|
|
306
|
+
visible = pendingNarration.replace(/^(?:\s*Tool results:\s*)+/i, '');
|
|
307
|
+
narrationIsNoise = false;
|
|
308
|
+
}
|
|
309
|
+
pendingNarration = '';
|
|
310
|
+
return visible;
|
|
311
|
+
}
|
|
312
|
+
function flushPendingNarrationIfAny() {
|
|
313
|
+
if (!pendingNarration)
|
|
314
|
+
return;
|
|
315
|
+
const stripped = pendingNarration.replace(/\s+/g, ' ').trim();
|
|
316
|
+
if (TOOL_RESULTS_NOISE_RE.test(stripped)) {
|
|
317
|
+
pendingNarration = '';
|
|
318
|
+
narrationIsNoise = false;
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const visible = pendingNarration;
|
|
322
|
+
pendingNarration = '';
|
|
323
|
+
narrationIsNoise = false;
|
|
324
|
+
writeAssistantText(visible);
|
|
325
|
+
}
|
|
277
326
|
const hooks = {
|
|
278
327
|
onText: (s) => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (inToolBlock && s.trim().length === 0)
|
|
284
|
-
return;
|
|
285
|
-
const followsToolBatch = inToolBlock;
|
|
286
|
-
// batch 收尾已经统一留了一条空白行。部分后端会把下一段正文以 \n / \n\n
|
|
287
|
-
// 开头发来;去掉这些“边界换行”,避免与 UI 分隔叠成两条空白行。
|
|
288
|
-
const visible = followsToolBatch ? s.replace(/^(?:[ \t]*\r?\n)+/, '') : s;
|
|
289
|
-
// 正文是工具批次边界:只有“连续且中间没有正文”的工具调用才合并。
|
|
290
|
-
// 一旦模型开始解释阶段结果,立即收尾当前摘要;后续工具重新建立批次。
|
|
291
|
-
if (s)
|
|
292
|
-
flushToolBatch();
|
|
293
|
-
spinner.stop(); // 任何正文 token 都停 spinner(首 token 停「思考中」;onToolCall 重启后若又来文本则停「生成中」)。未旋转时 stop 为 no-op。
|
|
294
|
-
layout.contentWriteMd(visible); // 正文走 markdown 渲染(代码块高亮 / 标题 / 列表 / 行内 …),见 ui/markdown.ts
|
|
295
|
-
if (visible) {
|
|
296
|
-
lastChar = visible[visible.length - 1];
|
|
297
|
-
if (visible.trim().length > 0) {
|
|
298
|
-
hasPendingTextBoundary = true;
|
|
299
|
-
textBoundaryNewlines = 0;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
328
|
+
const visible = trySuppressNoise(s);
|
|
329
|
+
if (visible === null)
|
|
330
|
+
return; // 纯噪声,抑制
|
|
331
|
+
writeAssistantText(visible);
|
|
302
332
|
},
|
|
303
333
|
onToolCall: (name) => {
|
|
334
|
+
// 工具调用开始前,先 flush 被抑制的叙述缓冲(若是纯噪声则丢弃)。
|
|
335
|
+
flushPendingNarrationIfAny();
|
|
304
336
|
// 文本/思考已流完,模型转而生成 tool_call 参数(可能很长,如 write_file 整篇内容):
|
|
305
337
|
// 补换行(让随后的 ● 行与 diff 不黏在正文末尾)+ 启「生成中」内联 spinner,内容区不再干等。
|
|
306
338
|
if (hasPendingTextBoundary) {
|
|
@@ -358,6 +390,7 @@ onContextUpdate) {
|
|
|
358
390
|
layout.contentWrite(`${ui.dim}${t('agent.aborted')}${ui.reset}\n`);
|
|
359
391
|
},
|
|
360
392
|
onDone: (elapsedMs, usage) => {
|
|
393
|
+
flushPendingNarrationIfAny();
|
|
361
394
|
flushToolBatch();
|
|
362
395
|
writeChangeOverview();
|
|
363
396
|
const tok = formatTurnTokens(usage);
|
|
@@ -427,9 +460,12 @@ function formatTurnTokens(usage) {
|
|
|
427
460
|
return '';
|
|
428
461
|
const fmt = (n) => (n < 1000 ? `${n}` : `${(n / 1000).toFixed(total >= 10000 ? 0 : 1)}k`);
|
|
429
462
|
const cached = usage.cachedTokens;
|
|
463
|
+
const cacheCreated = usage.cacheCreationTokens ?? 0;
|
|
430
464
|
const reasoning = usage.reasoningTokens;
|
|
431
465
|
const billablePrompt = usage.promptTokens - cached;
|
|
432
466
|
const extras = [];
|
|
467
|
+
if (cacheCreated > 0)
|
|
468
|
+
extras.push(`cache created ${fmt(cacheCreated)}`);
|
|
433
469
|
if (cached > 0)
|
|
434
470
|
extras.push(`${Math.round((cached / Math.max(1, usage.promptTokens)) * 100)}% cached`);
|
|
435
471
|
if (reasoning > 0)
|
package/dist/agent/spawn.js
CHANGED
|
@@ -250,6 +250,10 @@ export async function spawnAgent(opts) {
|
|
|
250
250
|
toolsOverride,
|
|
251
251
|
contextState: localContextState,
|
|
252
252
|
suppressOpeningAnalysis: true, // 子代理不注入「开场分析」:仅主线面对用户的首次响应用
|
|
253
|
+
// 子代理不注入主会话「会话状态」(plan + 笔记):systemPrompt 已用
|
|
254
|
+
// buildMocodeCorePrompt() 切掉会话私有尾段,工具表也排除了 plan_update——
|
|
255
|
+
// 灌主计划只会干扰窄 worker 并白付 token。
|
|
256
|
+
suppressSessionState: true,
|
|
253
257
|
onToolOutcome: (tool, args) => {
|
|
254
258
|
if (tool === 'read_file' && typeof args.path === 'string')
|
|
255
259
|
readSet.add(args.path);
|