mocode-ai 1.2.5 → 1.2.6
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 +9 -9
- package/dist/config/index.js +75 -11
- package/dist/context/encoders/table.js +1 -1
- package/dist/i18n/index.js +4 -4
- package/dist/llm/index.js +0 -1
- package/dist/memory/discover.js +8 -8
- package/dist/memory/index.js +3 -2
- package/dist/repl/index.js +12 -12
- package/dist/session/notes.js +233 -0
- package/dist/skills/runner.js +0 -1
- package/dist/tools/builtins/index.js +4 -0
- package/dist/tools/builtins/note-append.js +103 -0
- package/dist/tools/registry.js +13 -4
- package/dist/ui/diff.js +1 -1
- package/dist/ui/layout.js +1 -1
- package/dist/ui/render.js +7 -1
- package/package.json +3 -2
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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { getNotesMtime } from '../session/notes.js';
|
|
9
9
|
import { chat, estimatePromptTokens, planChatTools, chatTools, } from '../llm/index.js';
|
|
10
|
-
import { executeToolOutcome, getToolCapabilities, isFileMutationTool,
|
|
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';
|
|
@@ -18,7 +18,7 @@ import { createBudgetScheduler } from '../session/scheduler.js';
|
|
|
18
18
|
import { recordArtifact, invalidateArtifacts, rehydrateArtifacts, } 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, extractActivePlanSection, reinjectSessionStateIntoSystem } 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';
|
|
@@ -42,13 +42,13 @@ function parseArgs(raw) {
|
|
|
42
42
|
}
|
|
43
43
|
/** 只有显式声明 parallel 且无需权限确认的工具才进入普通并发组。 */
|
|
44
44
|
function isParallelTool(name) {
|
|
45
|
-
const tool =
|
|
45
|
+
const tool = findTool(name);
|
|
46
46
|
return !!tool && (tool.risk ?? 'safe') === 'safe' &&
|
|
47
47
|
getToolCapabilities(tool).concurrency === 'parallel';
|
|
48
48
|
}
|
|
49
49
|
/** resource-locked 工具先顺序完成权限预检,再依赖 canonical resource lock 并发执行。 */
|
|
50
50
|
function isResourceLockedTool(name) {
|
|
51
|
-
const tool =
|
|
51
|
+
const tool = findTool(name);
|
|
52
52
|
return !!tool && getToolCapabilities(tool).concurrency === 'resource-locked';
|
|
53
53
|
}
|
|
54
54
|
function isResourceLockedCall(call) {
|
|
@@ -326,8 +326,8 @@ export async function runAgentCore(opts) {
|
|
|
326
326
|
runtimeContextState.lifecycleStats = lifecycle.stats();
|
|
327
327
|
}
|
|
328
328
|
rehydrateArtifacts(runtimeContextState, history);
|
|
329
|
-
// ② compact
|
|
330
|
-
|
|
329
|
+
// ② compact 后把会话状态(活跃 plan + 笔记段)重注入系统提示,避免 agent 因上下文压缩丢失计划与笔记。
|
|
330
|
+
reinjectSessionStateIntoSystem(history);
|
|
331
331
|
}
|
|
332
332
|
hooks.onStepStart?.(); // 主 agent:spinner.start('思考中')
|
|
333
333
|
mode = 'idle';
|
|
@@ -602,7 +602,7 @@ export async function runAgentCore(opts) {
|
|
|
602
602
|
for (let k = 0; k < batch.length; k++) {
|
|
603
603
|
const tc = batch[k];
|
|
604
604
|
const parsed = parseArgs(tc.arguments);
|
|
605
|
-
const tool =
|
|
605
|
+
const tool = findTool(tc.name);
|
|
606
606
|
const argumentsValid = tool && parsed !== null
|
|
607
607
|
? validateToolArguments(tool, parsed).valid
|
|
608
608
|
: false;
|
|
@@ -693,7 +693,7 @@ export async function runAgentCore(opts) {
|
|
|
693
693
|
// 权限预检查:在渲染 ● 头之前弹确认面板(体验:先问再执行,而非执行完再问)。
|
|
694
694
|
// 拒绝时只渲染拒绝结果,不渲染执行头;放行则继续走 header → start → executeTool 流程。
|
|
695
695
|
const parsed = parseArgs(tc.arguments);
|
|
696
|
-
const tool =
|
|
696
|
+
const tool = findTool(tc.name);
|
|
697
697
|
const argumentsValid = tool && parsed !== null
|
|
698
698
|
? validateToolArguments(tool, parsed).valid
|
|
699
699
|
: false;
|
|
@@ -768,7 +768,7 @@ export async function runAgentCore(opts) {
|
|
|
768
768
|
// B(nag 提醒):连续 N 步有工具活动但没更新 plan,在当前步第一条 tool_result 前注入提醒。
|
|
769
769
|
const notesMtimeAfter = getNotesMtime();
|
|
770
770
|
if (notesMtimeAfter !== notesMtimeBefore) {
|
|
771
|
-
|
|
771
|
+
reinjectSessionStateIntoSystem(history);
|
|
772
772
|
stepsSincePlanTouch = 0;
|
|
773
773
|
}
|
|
774
774
|
else {
|
package/dist/config/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
5
|
import { getCurrentSessionId } from '../session/state.js';
|
|
6
|
-
import { getNotesFilePath } from '../session/notes.js';
|
|
6
|
+
import { getNotesFilePath, extractActiveNotesSections } from '../session/notes.js';
|
|
7
7
|
import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
|
|
8
8
|
import { detectLanguage, setLanguage, t, } from '../i18n/index.js';
|
|
9
9
|
/**
|
|
@@ -214,20 +214,81 @@ export function reinjectActivePlanIntoSystem(history) {
|
|
|
214
214
|
sys.content = `${content}${ACTIVE_PLAN_MARKER}${plan}\n`;
|
|
215
215
|
return true;
|
|
216
216
|
}
|
|
217
|
+
/** compact 重注入用的幂等标记:history[0] 中夹住会话笔记段正文(Findings/Decisions/Open Questions/Risks),
|
|
218
|
+
* 重复注入只替换不累积。与 ACTIVE_PLAN_MARKER 独立,互不干扰。 */
|
|
219
|
+
const NOTES_BODY_MARKER = '\n\n<!-- mocode:session-notes -->\n';
|
|
220
|
+
/**
|
|
221
|
+
* 把会话笔记段正文重注入系统提示(history[0])。compact 后或本步改了 notes.md 时调用:
|
|
222
|
+
* 若 notes.md 有活跃笔记段(extractActiveNotesSections 返回非空,已按 5k token 预算裁剪),
|
|
223
|
+
* 则覆盖旧标记块写入最新内容;若无,则清掉残留标记块。直接改 history[0].content,
|
|
224
|
+
* 幂等,返回是否改动。与 reinjectActivePlanIntoSystem 独立:plan 段由后者管,笔记段由本函数管。
|
|
225
|
+
*/
|
|
226
|
+
export function reinjectSessionNotesIntoSystem(history) {
|
|
227
|
+
const sys = history[0];
|
|
228
|
+
if (!sys || sys.role !== 'system' || typeof sys.content !== 'string')
|
|
229
|
+
return false;
|
|
230
|
+
let content = sys.content;
|
|
231
|
+
const markerIdx = content.indexOf(NOTES_BODY_MARKER);
|
|
232
|
+
if (markerIdx >= 0) {
|
|
233
|
+
content = content.slice(0, markerIdx).replace(/\s+$/, '');
|
|
234
|
+
}
|
|
235
|
+
const notes = extractActiveNotesSections();
|
|
236
|
+
if (!notes) {
|
|
237
|
+
if (markerIdx < 0)
|
|
238
|
+
return false;
|
|
239
|
+
sys.content = content;
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
sys.content = `${content}${NOTES_BODY_MARKER}${notes}\n`;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 一次性重注入会话状态(plan 段 + 笔记段)到系统提示。调用点(core.ts compact 后 /
|
|
247
|
+
* 本步改 notes.md、repl compact 时)统一用本函数替代 reinjectActivePlanIntoSystem,
|
|
248
|
+
* 让笔记段享有与 plan 段完全对称的常驻 + 恢复时机。返回任一 marker 是否改动。
|
|
249
|
+
*/
|
|
250
|
+
export function reinjectSessionStateIntoSystem(history) {
|
|
251
|
+
const planChanged = reinjectActivePlanIntoSystem(history);
|
|
252
|
+
const notesChanged = reinjectSessionNotesIntoSystem(history);
|
|
253
|
+
return planChanged || notesChanged;
|
|
254
|
+
}
|
|
217
255
|
const SYSTEM_PROMPT_MEMORY_SECTION = `
|
|
218
256
|
## Memory (cross-session facts)
|
|
219
257
|
- The prompt may contain a title/summary index; retrieve details with memory_search or inspect all with memory_list. memory_search also surfaces knowledge-graph facts (relations between entities) alongside entry bodies.
|
|
220
258
|
- Save only stable, non-obvious cross-session facts. Search before saving; update an existing entry instead of duplicating it, and archive stale entries.
|
|
221
259
|
- A knowledge-graph layer links entities across memories: explore relations/neighbors with memory_graph (neighbors/add/stats), and attach meaningful links via the links parameter of memory_save when saving.`;
|
|
222
|
-
/**
|
|
260
|
+
/** AGENTS.md 自动导入正文上限:system 位于 history[0] 且 compactHistory 不压缩 system,超长需截断防占窗口(见 memory/README.md)。 */
|
|
261
|
+
const MAX_AGENTS_IMPORT_CHARS = 20000;
|
|
262
|
+
/**
|
|
263
|
+
* 工作区根 AGENTS.md 自动导入段:与 memory 开关完全无关——
|
|
264
|
+
* 只要 <cwd>/AGENTS.md 存在就把正文直接拼进 prompt(超 {@link MAX_AGENTS_IMPORT_CHARS} 截断+末尾提示),
|
|
265
|
+
* 不再只指路让模型按需 read_file。读失败静默跳过(返空串)。
|
|
266
|
+
*/
|
|
267
|
+
function buildAgentsImportSection() {
|
|
268
|
+
try {
|
|
269
|
+
const projectAgents = path.join(process.cwd(), 'AGENTS.md');
|
|
270
|
+
if (!fs.existsSync(projectAgents))
|
|
271
|
+
return '';
|
|
272
|
+
const content = fs.readFileSync(projectAgents, 'utf8').trim();
|
|
273
|
+
if (!content)
|
|
274
|
+
return '';
|
|
275
|
+
const body = content.length > MAX_AGENTS_IMPORT_CHARS
|
|
276
|
+
? `${content.slice(0, MAX_AGENTS_IMPORT_CHARS)}\n…[AGENTS.md truncated: first ${MAX_AGENTS_IMPORT_CHARS} characters injected]`
|
|
277
|
+
: content;
|
|
278
|
+
return `\n## Project memory (AGENTS.md, auto-imported)\n${body}\n- AGENTS.md may be stale: current code and the user request override stale memory.`;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return ''; // 读失败静默跳过:不让导入破坏 prompt 构建
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Memory 检索指导段(与 AGENTS.md 导入无关):开 isMemoryEnabled() 时才拼,
|
|
286
|
+
* 注入 memory_search/list/graph 的使用指导。默认关(新用户零侵入)。
|
|
287
|
+
*/
|
|
223
288
|
function buildMemoryPromptSection() {
|
|
224
289
|
if (!isMemoryEnabled())
|
|
225
290
|
return '';
|
|
226
|
-
|
|
227
|
-
const mocodeHint = fs.existsSync(projectMocode)
|
|
228
|
-
? '\n- `MOCODE.md` exists at the workspace root but is not preloaded. Read it with `read_file` only when the task may depend on project architecture, conventions, commands, prior decisions, or user preferences; skip it for greetings and unrelated simple requests. Current code and the user request override stale memory.'
|
|
229
|
-
: '';
|
|
230
|
-
return SYSTEM_PROMPT_MEMORY_SECTION + mocodeHint;
|
|
291
|
+
return SYSTEM_PROMPT_MEMORY_SECTION;
|
|
231
292
|
}
|
|
232
293
|
/**
|
|
233
294
|
* plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
|
|
@@ -258,6 +319,7 @@ ${buildPlanResearchRules()}`;
|
|
|
258
319
|
}
|
|
259
320
|
/** 兼容旧名字:repl 的 buildSystemMessage 仍引 PLAN_MODE_SUFFIX(变量)。运行时按需现拼。 */
|
|
260
321
|
export function buildBasePrompt(sessionId = getCurrentSessionId()) {
|
|
322
|
+
const agentsImportSection = buildAgentsImportSection();
|
|
261
323
|
const memorySection = buildMemoryPromptSection();
|
|
262
324
|
const notepadSection = buildNotepadSection(sessionId);
|
|
263
325
|
// 静态主体:稳定段落集中在前,让支持 prompt caching 的后端能命中前缀缓存(#12)。
|
|
@@ -313,9 +375,9 @@ ${buildVoiceSection()}
|
|
|
313
375
|
- **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
|
|
314
376
|
- Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.
|
|
315
377
|
${t('assistant.languageInstruction')}`;
|
|
316
|
-
// 动态段(置于末尾):memory 索引 + notepad 索引 + notepad 使用说明。
|
|
378
|
+
// 动态段(置于末尾):AGENTS.md 项目记忆(无条件) + memory 索引(按开关) + notepad 索引 + notepad 使用说明。
|
|
317
379
|
// 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
|
|
318
|
-
// - "## Project context" 仅当 memorySection/notepadSection
|
|
380
|
+
// - "## Project context" 仅当 agentsImportSection/memorySection/notepadSection 任一非空(notepad 索引依赖 notes.md 存在);
|
|
319
381
|
// - "## Session state" 使用说明**无条件**注入(放在动态尾段首位):否则会陷入"说明依赖 notes.md 存在 → 模型不知要建 → 文件永不存在"的鸡生蛋循环,功能对模型不可见。动态段在静态前缀之后,不影响 prompt 缓存。
|
|
320
382
|
const dynamicParts = [];
|
|
321
383
|
// 会话级私有尾段(子 agent 切片会丢弃):Session state 说明无条件注入在前,Project context 按需在后。
|
|
@@ -333,8 +395,10 @@ ${t('assistant.languageInstruction')}`;
|
|
|
333
395
|
'Keep at most one step in_progress, and mark a step completed as soon as its work is done — do not batch updates to the end of the turn. ' +
|
|
334
396
|
'Write each step so a teammate who lost the conversation could pick it up cold: name the file or symbol, the exact change, and the verification, so the plan survives context compaction. ' +
|
|
335
397
|
'plan_update creates notes.md for you when the task warrants it; read_file the full notes.md whenever you need to recover context after compaction. ' +
|
|
336
|
-
'When every step is completed, plan_update settles the plan to `## Done:` automatically. Keep other notes concise and session-specific; use memory for stable cross-session facts
|
|
337
|
-
|
|
398
|
+
'When every step is completed, plan_update settles the plan to `## Done:` automatically. Keep other notes concise and session-specific; use memory for stable cross-session facts.\n' +
|
|
399
|
+
'## Session notes (resident memory)\n' +
|
|
400
|
+
'For non-obvious, lasting-value discoveries — subtle constraints, decisions with downstream impact, open questions blocking a choice, or risks affecting later steps — call `note_append` IMMEDIATELY when you make the discovery. The note is written to the same notes.md and its body is re-injected into the prompt automatically (within a 5k-token budget), surviving compaction so you keep remembering what you found/decided this session. Do NOT use it for routine progress (that is the plan) or stable cross-session facts (that is memory_save). Each call appends one item.');
|
|
401
|
+
const ctxContent = `${agentsImportSection}${memorySection}${notepadSection}`.trimEnd();
|
|
338
402
|
if (ctxContent) {
|
|
339
403
|
dynamicParts.push(`## Project context\n${ctxContent}`);
|
|
340
404
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 顶部加 `# N entries · table-encoded` 计数头。
|
|
7
7
|
*
|
|
8
8
|
* 不变量(离线脚本断言):id/name/summary/type 全保留;仅 `active` 状态被省略(默认值,无信息损失)。
|
|
9
|
-
* 正则锚定整行 + 要求 `id: ... — ... (type, active)` 形,不匹配的行原样返回(防误伤
|
|
9
|
+
* 正则锚定整行 + 要求 `id: ... — ... (type, active)` 形,不匹配的行原样返回(防误伤 AGENTS.md 等正文)。
|
|
10
10
|
* group1 贪婪捕获到 `(\w+` 为止,故 name/summary 内含 ` — ` 或 ` (` 也不影响(仅丢尾部 `, active)`)。
|
|
11
11
|
*/
|
|
12
12
|
const ACTIVE_LINE_RE = /^(- [^:]+: .+ — .+ \(\w+), active\)$/;
|
package/dist/i18n/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const zhCN = {
|
|
|
18
18
|
'commands.memoryOff': '关闭记忆子系统',
|
|
19
19
|
'commands.memoryStatus': '查看当前开关与原理',
|
|
20
20
|
'commands.memoryReflect': '手动触发后台记忆反思',
|
|
21
|
-
'commands.memoryInit': '扫描项目生成
|
|
21
|
+
'commands.memoryInit': '扫描项目生成 AGENTS.md 项目记忆',
|
|
22
22
|
'commands.subagent': '子 Agent 开关(默认关闭)',
|
|
23
23
|
'commands.subagentOn': '开启子 Agent',
|
|
24
24
|
'commands.subagentOff': '关闭子 Agent',
|
|
@@ -69,7 +69,7 @@ const zhCN = {
|
|
|
69
69
|
'running.rollback': '回滚',
|
|
70
70
|
'running.chooseTurn': '选择轮次…',
|
|
71
71
|
'running.init': '初始化',
|
|
72
|
-
'running.generateMemory': '生成
|
|
72
|
+
'running.generateMemory': '生成 AGENTS.md…',
|
|
73
73
|
'running.plan': '切 plan',
|
|
74
74
|
'running.auto': '切 auto',
|
|
75
75
|
'running.clear': '清空',
|
|
@@ -277,7 +277,7 @@ const en = {
|
|
|
277
277
|
'commands.memoryOff': 'Disable the memory subsystem',
|
|
278
278
|
'commands.memoryStatus': 'Show current state and behavior',
|
|
279
279
|
'commands.memoryReflect': 'Run background memory reflection',
|
|
280
|
-
'commands.memoryInit': 'Scan project and generate
|
|
280
|
+
'commands.memoryInit': 'Scan project and generate AGENTS.md',
|
|
281
281
|
'commands.subagent': 'Sub-agent controls (disabled by default)',
|
|
282
282
|
'commands.subagentOn': 'Enable sub-agents',
|
|
283
283
|
'commands.subagentOff': 'Disable sub-agents',
|
|
@@ -328,7 +328,7 @@ const en = {
|
|
|
328
328
|
'running.rollback': 'Rollback',
|
|
329
329
|
'running.chooseTurn': 'Choose a turn…',
|
|
330
330
|
'running.init': 'Initialize',
|
|
331
|
-
'running.generateMemory': 'Generating
|
|
331
|
+
'running.generateMemory': 'Generating AGENTS.md…',
|
|
332
332
|
'running.plan': 'Plan mode',
|
|
333
333
|
'running.auto': 'Auto mode',
|
|
334
334
|
'running.clear': 'Clear',
|
package/dist/llm/index.js
CHANGED
|
@@ -180,7 +180,6 @@ export function refreshChatTools() {
|
|
|
180
180
|
// MCP 协议没有可靠的副作用注解;plan 模式绝不暴露外部工具,保留只读探查保证。
|
|
181
181
|
planChatTools.splice(0, planChatTools.length, ...next.filter((t) => !t.function.name.startsWith('mcp__') && !getPlanDisabledTools().has(t.function.name)));
|
|
182
182
|
}
|
|
183
|
-
refreshChatTools();
|
|
184
183
|
/**
|
|
185
184
|
* 多 provider 兼容的 cache / reasoning 字段提取。
|
|
186
185
|
* 不同后端报 cached 字段名差异巨大,这里按"最常见的几种"顺序 probe,
|
package/dist/memory/discover.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
// memory 发现子系统:加载项目记忆
|
|
1
|
+
// memory 发现子系统:加载项目记忆 AGENTS.md(对标 skills/discover.ts 的叶子模式)。
|
|
2
2
|
// 仅依赖 node 标准库,是叶子模块:不依赖 config/agent/llm/tools/skills,避免环。
|
|
3
3
|
//
|
|
4
|
-
// 约定:纯
|
|
5
|
-
// 项目级从 cwd 向上逐级找,全局 ~/.mocode/
|
|
4
|
+
// 约定:纯 AGENTS.md(mocode 是独立工具,有自己的工具集与约定)。
|
|
5
|
+
// 项目级从 cwd 向上逐级找,全局 ~/.mocode/AGENTS.md。全量注入 systemPrompt(超长截断);
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
/**
|
|
10
|
-
* 返回要查找的
|
|
11
|
-
* 全局 ~/.mocode/
|
|
10
|
+
* 返回要查找的 AGENTS.md 路径列表,按「远→近」顺序(合并时近的在后,更突出):
|
|
11
|
+
* 全局 ~/.mocode/AGENTS.md → 项目级从根到 cwd 逐级 AGENTS.md。
|
|
12
12
|
* 向上遍历 cwd 到根收集 [cwd..root],反转为 [root..cwd](远→近),前拼全局。
|
|
13
13
|
*/
|
|
14
14
|
export function resolveMemoryFiles() {
|
|
15
|
-
const globalPath = path.join(os.homedir(), '.mocode', '
|
|
15
|
+
const globalPath = path.join(os.homedir(), '.mocode', 'AGENTS.md');
|
|
16
16
|
const projectFiles = [];
|
|
17
17
|
let dir = process.cwd();
|
|
18
18
|
const root = path.parse(dir).root; // win32 'C:\\', POSIX '/'
|
|
19
19
|
for (;;) {
|
|
20
|
-
projectFiles.push(path.join(dir, '
|
|
20
|
+
projectFiles.push(path.join(dir, 'AGENTS.md'));
|
|
21
21
|
if (dir === root || dir === path.dirname(dir))
|
|
22
22
|
break; // 到根:dirname 自身
|
|
23
23
|
dir = path.dirname(dir);
|
|
@@ -26,7 +26,7 @@ export function resolveMemoryFiles() {
|
|
|
26
26
|
return [globalPath, ...projectFiles];
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* 读取所有存在的
|
|
29
|
+
* 读取所有存在的 AGENTS.md,返回 { path, content }(content 已 trim)。
|
|
30
30
|
* 全程静默容错(不存在 / 读失败 → 跳过,不抛),风格对齐 skills/discover.ts 与 session/persist.ts。
|
|
31
31
|
*/
|
|
32
32
|
export function loadMemoryFiles() {
|
package/dist/memory/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Memory barrel: Tier-2 JSONL store + knowledge-graph layer + background reflection.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// AGENTS.md is intentionally not loaded here either: config/index.ts
|
|
3
|
+
// (buildAgentsImportSection) auto-imports the workspace-root AGENTS.md body into the
|
|
4
|
+
// system prompt — independent of the memory switch — truncating it when it exceeds the cap.
|
|
4
5
|
export { buildMemoryIndexSection, loadAll, gcMemories, } from './store.js';
|
|
5
6
|
export { addTriple, upsertEntity, findEntity, searchGraph, neighborsOf, pathBetween, graphStats, } from './graph.js';
|
|
6
7
|
export { kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, runReflection, } from './reflect.js';
|
package/dist/repl/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, isFrontendToolsEnabled, updateFrontendToolsConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, hasCodegraphIndex,
|
|
4
|
+
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, isFrontendToolsEnabled, updateFrontendToolsConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, hasCodegraphIndex, reinjectSessionStateIntoSystem, DEFAULT_CONTEXT_WINDOW_TOKENS, } from '../config/index.js';
|
|
5
5
|
import { getLanguage, normalizeLanguage, t, } from '../i18n/index.js';
|
|
6
6
|
import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
|
|
7
7
|
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
@@ -66,9 +66,9 @@ function buildSlashCommands() {
|
|
|
66
66
|
{ name: 'off', value: '/memory_switch off', desc: d('commands.memoryOff') },
|
|
67
67
|
{ name: 'status', value: '/memory_status', desc: d('commands.memoryStatus') },
|
|
68
68
|
{ name: 'reflect', value: '/reflect', desc: d('commands.memoryReflect') },
|
|
69
|
-
{ name: 'init', value: '/init', desc: d('commands.memoryInit') },
|
|
70
69
|
],
|
|
71
70
|
},
|
|
71
|
+
{ name: '/init', desc: d('commands.memoryInit') },
|
|
72
72
|
{
|
|
73
73
|
name: '/subagent', desc: d('commands.subagent'), children: [
|
|
74
74
|
{ name: 'on', value: '/subagent on', desc: d('commands.subagentOn') },
|
|
@@ -173,7 +173,7 @@ function maskKey(k) {
|
|
|
173
173
|
return `${'='.repeat(Math.min(k.length - 4, 20))}${k.slice(-4)}`;
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
176
|
-
* /init 指令:发给 agent 扫描项目并生成
|
|
176
|
+
* /init 指令:发给 agent 扫描项目并生成 AGENTS.md。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
|
|
177
177
|
*
|
|
178
178
|
* 函数化(非 const):.codegraph/ 索引是否存在的探测放在调用瞬间,没索引时不提 codegraph,
|
|
179
179
|
* 避免 LLM 调出失败。/init 是冷启动动作,IO 开销可忽略。
|
|
@@ -182,14 +182,14 @@ function buildInitPrompt() {
|
|
|
182
182
|
const cg = hasCodegraphIndex()
|
|
183
183
|
? '- 若有 .codegraph/:用 use_skill 加载 codegraph skill 后用 run_command 调 codegraph explore "<架构或入口符号>" 一次拿相关源码+调用路径,别逐文件读!!!\n'
|
|
184
184
|
: '';
|
|
185
|
-
return `分析当前项目(process.cwd()),生成
|
|
185
|
+
return `分析当前项目(process.cwd()),生成 AGENTS.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
|
|
186
186
|
|
|
187
187
|
先探查(尽量少调用拿全貌):
|
|
188
188
|
${cg}- read_file package.json(或 Cargo.toml/pyproject.toml/go.mod 等):scripts、依赖、入口、模块类型。
|
|
189
189
|
- glob 顶层目录;read_file 入口文件 + 各子系统 index.ts/README。
|
|
190
|
-
- 若
|
|
190
|
+
- 若 AGENTS.md 已存在:read_file 读它,在其基础上更新(补缺、修正过时),不丢已有准确事实。
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
AGENTS.md 按以下结构写(每节简短,只写稳定、非显然的事实):
|
|
193
193
|
## 项目
|
|
194
194
|
一两句:是什么、技术栈、运行环境。
|
|
195
195
|
## 命令
|
|
@@ -204,7 +204,7 @@ install / dev / build / test / typecheck / lint 等——从 package.json script
|
|
|
204
204
|
硬要求:
|
|
205
205
|
- 从实际代码提炼,引用具体文件名/命令/符号;不编造、不泛泛。
|
|
206
206
|
- 总长 ≤ 3000 字;只写后续会话有用的稳定事实,不写易变项(当前 bug、临时文件、未决 TODO)。
|
|
207
|
-
- 用 write_file 写入项目根
|
|
207
|
+
- 用 write_file 写入项目根 AGENTS.md。
|
|
208
208
|
- 写完简述:写了哪几节 + 从代码里发现的 2-3 条非显然关键约定(供用户校验)。`;
|
|
209
209
|
}
|
|
210
210
|
/** 临时 readline 读一行(cooked,用于子提问;主输入走 promptWithSlashMenu)。 */
|
|
@@ -768,7 +768,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
768
768
|
//
|
|
769
769
|
// 与开关联动:① base 用 buildBasePrompt() 取代 config.systemPrompt(后者是启动时一次性
|
|
770
770
|
// 求值的常量,运行时 /memory_switch 不会刷新);② plan suffix 走 getPlanModeSuffix() 现拼;
|
|
771
|
-
// ③
|
|
771
|
+
// ③ AGENTS.md 存在工作区根时由 base 无条件自动导入正文(超长截断,与 memory 开关无关);④ Memory Index 按开关注入。
|
|
772
772
|
const buildSystemMessage = (planMode) => effectiveSystemPrompt(buildBasePrompt(currentSessionId) +
|
|
773
773
|
(planMode ? getPlanModeSuffix() : '') +
|
|
774
774
|
buildMemoryIndexSection(isMemoryEnabled()));
|
|
@@ -1199,7 +1199,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1199
1199
|
continue;
|
|
1200
1200
|
}
|
|
1201
1201
|
if (line === '/init') {
|
|
1202
|
-
// /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成
|
|
1202
|
+
// /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 AGENTS.md),fall through 走 runAgent
|
|
1203
1203
|
joined = buildInitPrompt();
|
|
1204
1204
|
}
|
|
1205
1205
|
if (line === '/upgrade' || line.startsWith('/upgrade ')) {
|
|
@@ -1438,7 +1438,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1438
1438
|
layout.contentWrite(` ${ui.accent}${e.id}${ui.reset} ${ui.dim}${e.name} — ${e.summary}${ui.reset}\n`);
|
|
1439
1439
|
}
|
|
1440
1440
|
if (active.length === 0)
|
|
1441
|
-
layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成
|
|
1441
|
+
layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成 AGENTS.md)${ui.reset}\n`);
|
|
1442
1442
|
layout.contentWrite(`${ui.dim}(详情用 memory_search;启动索引已注入 systemPrompt)${ui.reset}\n`);
|
|
1443
1443
|
continue;
|
|
1444
1444
|
}
|
|
@@ -1545,9 +1545,9 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1545
1545
|
// focus 透传到 compact_history action 的 LLM 摘要 prompt。
|
|
1546
1546
|
// 返回 SchedulerRunLog 给 UI 显示决策;退化路径(开关关时)在 manualCompact 内部走 compactHistory。
|
|
1547
1547
|
const log = await manualCompact(history, focus, { force });
|
|
1548
|
-
// ② compact
|
|
1548
|
+
// ② compact 后把会话状态(plan + 笔记段)重注入系统提示(history[0]),避免 agent 因上下文压缩丢失计划与笔记。
|
|
1549
1549
|
if (log.compactHistoryCalled)
|
|
1550
|
-
|
|
1550
|
+
reinjectSessionStateIntoSystem(history);
|
|
1551
1551
|
const d = log.compactDetail;
|
|
1552
1552
|
appendCurrentSessionRuntimeEvent('compact', {
|
|
1553
1553
|
source: 'manual',
|
package/dist/session/notes.js
CHANGED
|
@@ -105,3 +105,236 @@ export function writePlanToNotes(plan, sessionId = getCurrentSessionId()) {
|
|
|
105
105
|
}
|
|
106
106
|
return { path: p, settled: plan.steps.length > 0 && plan.steps.every((s) => s.status === 'completed') };
|
|
107
107
|
}
|
|
108
|
+
// ── Session notes(单会话永久记忆):note_append 写入,reinject 常驻 system ──────
|
|
109
|
+
// 设计:notes.md 不止放 Plan。note_append 往预设笔记段追加一条 finding/decision/
|
|
110
|
+
// open_question/risk;extractActiveNotesSections 读出活跃笔记段正文(排除 Plan/Done),
|
|
111
|
+
// 按 5k token 预算裁剪后由 reinjectSessionStateIntoSystem 注入 system prompt——compact
|
|
112
|
+
// 后仍能恢复,让 agent 始终记得本会话做过什么、发现过什么(单会话永久记忆)。
|
|
113
|
+
/** note_append 接受的预设段 key → 渲染标题。 */
|
|
114
|
+
const NOTE_SECTION_TITLES = {
|
|
115
|
+
findings: 'Findings',
|
|
116
|
+
decisions: 'Decisions',
|
|
117
|
+
open_questions: 'Open Questions',
|
|
118
|
+
risks: 'Risks',
|
|
119
|
+
};
|
|
120
|
+
/** 预设段 key 列表(供工具 schema enum 与校验用)。 */
|
|
121
|
+
export const NOTE_SECTION_KEYS = Object.keys(NOTE_SECTION_TITLES);
|
|
122
|
+
/** 段注入优先级:数值越大越先占预算、越后丢弃正文。Risks 最重要。 */
|
|
123
|
+
const SECTION_PRIORITY = {
|
|
124
|
+
risks: 4, findings: 3, decisions: 2, open_questions: 1,
|
|
125
|
+
};
|
|
126
|
+
/** 常驻笔记正文总预算(token)。5k:占百万级 context 的 0.5%,可常驻相当量笔记。 */
|
|
127
|
+
const NOTES_INJECT_BUDGET_TOKENS = 5000;
|
|
128
|
+
/** 单段正文上限(token):防单段独占预算。 */
|
|
129
|
+
const NOTES_PER_SECTION_TOKENS = 2000;
|
|
130
|
+
/** 单条笔记上限(token):防一条过长吃掉整段预算。 */
|
|
131
|
+
const NOTES_PER_ENTRY_TOKENS = 800;
|
|
132
|
+
/**
|
|
133
|
+
* 轻量 token 估算(启发式,不依赖 tokenizer):CJK ≈ 0.6 token/字,
|
|
134
|
+
* ASCII ≈ 0.25 token/字。对 GLM/DeepSeek/Qwen 等中文偏多的后端略偏保守
|
|
135
|
+
* (估算略高于实际 → 注入实际 token 略低于预算 → 安全侧)。仅供笔记 cap 用,
|
|
136
|
+
* 不替换 llm/estimatePromptTokens 的主口径。
|
|
137
|
+
*/
|
|
138
|
+
function estimateTokens(text) {
|
|
139
|
+
let cjk = 0;
|
|
140
|
+
let other = 0;
|
|
141
|
+
for (let i = 0; i < text.length; i++) {
|
|
142
|
+
const c = text.charCodeAt(i);
|
|
143
|
+
if ((c >= 0x3000 && c <= 0x9fff) || (c >= 0xff00 && c <= 0xffef))
|
|
144
|
+
cjk++;
|
|
145
|
+
else
|
|
146
|
+
other++;
|
|
147
|
+
}
|
|
148
|
+
return Math.ceil(cjk * 0.6 + other * 0.25);
|
|
149
|
+
}
|
|
150
|
+
/** 把标题映射回预设 key(非预设段返 '',优先级 0,最后注入)。 */
|
|
151
|
+
function matchSectionKey(title) {
|
|
152
|
+
const t = title.trim().toLowerCase();
|
|
153
|
+
for (const [k, v] of Object.entries(NOTE_SECTION_TITLES)) {
|
|
154
|
+
if (v.toLowerCase() === t)
|
|
155
|
+
return k;
|
|
156
|
+
}
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 往 notes.md 的指定笔记段追加一条。段存在则在其末尾追加(保留其它段不动);
|
|
161
|
+
* 段不存在则在文件末新建。返回 { path } 或 { error }。
|
|
162
|
+
*/
|
|
163
|
+
export function appendNoteToSection(section, entry, tag, sessionId = getCurrentSessionId()) {
|
|
164
|
+
const title = NOTE_SECTION_TITLES[section];
|
|
165
|
+
if (!title)
|
|
166
|
+
return { error: `unknown note section "${section}"` };
|
|
167
|
+
const p = getNotesFilePath(sessionId);
|
|
168
|
+
if (!p)
|
|
169
|
+
return { error: 'no active session' };
|
|
170
|
+
const line = tag ? `- **[${tag}]** ${entry}` : `- ${entry}`;
|
|
171
|
+
let existing = '';
|
|
172
|
+
try {
|
|
173
|
+
existing = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
existing = '';
|
|
177
|
+
}
|
|
178
|
+
const header = `## ${title}`;
|
|
179
|
+
const lines = existing.split('\n');
|
|
180
|
+
const start = lines.findIndex((l) => l.trim() === header);
|
|
181
|
+
let next;
|
|
182
|
+
if (start >= 0) {
|
|
183
|
+
// 段末 = 下一个 ## 或文件末
|
|
184
|
+
let end = lines.length;
|
|
185
|
+
for (let k = start + 1; k < lines.length; k++) {
|
|
186
|
+
if (/^##\s/.test(lines[k])) {
|
|
187
|
+
end = k;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const before = lines.slice(0, start).join('\n').replace(/\s+$/, '');
|
|
192
|
+
const sectionLines = lines.slice(start, end);
|
|
193
|
+
// 去段尾空行后追加新条目
|
|
194
|
+
while (sectionLines.length && sectionLines[sectionLines.length - 1].trim() === '')
|
|
195
|
+
sectionLines.pop();
|
|
196
|
+
sectionLines.push(line);
|
|
197
|
+
const section = sectionLines.join('\n');
|
|
198
|
+
const after = lines.slice(end).join('\n').replace(/^\s+/, '');
|
|
199
|
+
next = [before, section, after].filter((s) => s.length > 0).join('\n\n') + '\n';
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// 新建段:放文件末,与已有内容以空行分隔
|
|
203
|
+
const rest = existing.trim();
|
|
204
|
+
const newSection = `${header}\n${line}`;
|
|
205
|
+
next = rest ? `${rest}\n\n${newSection}\n` : `${newSection}\n`;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
209
|
+
fs.writeFileSync(p, next, 'utf8');
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
213
|
+
}
|
|
214
|
+
return { path: p };
|
|
215
|
+
}
|
|
216
|
+
/** 按字符二分截断条目到 token 上限,加省略标记。条目不长,线性二分足够。 */
|
|
217
|
+
function truncateEntry(entry, maxTokens) {
|
|
218
|
+
if (estimateTokens(entry) <= maxTokens)
|
|
219
|
+
return entry;
|
|
220
|
+
let lo = 0;
|
|
221
|
+
let hi = entry.length;
|
|
222
|
+
while (lo < hi) {
|
|
223
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
224
|
+
if (estimateTokens(entry.slice(0, mid)) <= maxTokens)
|
|
225
|
+
lo = mid;
|
|
226
|
+
else
|
|
227
|
+
hi = mid - 1;
|
|
228
|
+
}
|
|
229
|
+
return entry.slice(0, lo).replace(/\s+$/, '') + ' …[truncated]';
|
|
230
|
+
}
|
|
231
|
+
/** 把单段正文按条目分割,从末尾(最近)保留,丢最旧,裁到 token 预算内。 */
|
|
232
|
+
function trimSectionToBudget(body, budgetTokens) {
|
|
233
|
+
if (budgetTokens <= 0)
|
|
234
|
+
return null;
|
|
235
|
+
const bodyLines = body.split('\n');
|
|
236
|
+
const header = bodyLines[0] ?? '';
|
|
237
|
+
const rest = bodyLines.slice(1);
|
|
238
|
+
// 分条目:以 "- " 开头为一条起始,后续非 "- " 行归入该条
|
|
239
|
+
const entries = [];
|
|
240
|
+
let cur = [];
|
|
241
|
+
for (const ln of rest) {
|
|
242
|
+
if (/^-\s+/.test(ln)) {
|
|
243
|
+
if (cur.length)
|
|
244
|
+
entries.push(cur.join('\n'));
|
|
245
|
+
cur = [ln];
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
cur.push(ln);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (cur.length)
|
|
252
|
+
entries.push(cur.join('\n'));
|
|
253
|
+
const kept = [];
|
|
254
|
+
let used = estimateTokens(header);
|
|
255
|
+
for (let k = entries.length - 1; k >= 0; k--) {
|
|
256
|
+
let e = entries[k];
|
|
257
|
+
if (estimateTokens(e) > NOTES_PER_ENTRY_TOKENS) {
|
|
258
|
+
e = truncateEntry(e, NOTES_PER_ENTRY_TOKENS);
|
|
259
|
+
}
|
|
260
|
+
const t = estimateTokens(e);
|
|
261
|
+
if (used + t > budgetTokens)
|
|
262
|
+
break;
|
|
263
|
+
kept.unshift(e);
|
|
264
|
+
used += t;
|
|
265
|
+
}
|
|
266
|
+
if (kept.length === 0)
|
|
267
|
+
return null;
|
|
268
|
+
return `${header}\n${kept.join('\n')}`;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 读 notes.md,提取所有活跃笔记段正文(排除 `## Plan:` 与 `## Done:`),
|
|
272
|
+
* 按 NOTES_INJECT_BUDGET_TOKENS 裁剪后返回——供 reinject 注入 system prompt。
|
|
273
|
+
* 裁剪策略:段按优先级排序(Risks>Findings>Decisions>Open Questions>自定义),
|
|
274
|
+
* 逐段注入累计 token;单段超 per-section 则段内从最近条目保留丢最旧;
|
|
275
|
+
* 总预算用尽则后续段不注入正文(其标题仍由 buildNotepadSection 索引常驻,
|
|
276
|
+
* agent 可 read_file 取细节)。这样 5k 预算内"写了就常驻",超出降级为索引+按需 read。
|
|
277
|
+
*/
|
|
278
|
+
export function extractActiveNotesSections(budget = NOTES_INJECT_BUDGET_TOKENS, sessionId = getCurrentSessionId()) {
|
|
279
|
+
const p = getNotesFilePath(sessionId);
|
|
280
|
+
if (!p)
|
|
281
|
+
return '';
|
|
282
|
+
let content = '';
|
|
283
|
+
try {
|
|
284
|
+
content = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return '';
|
|
288
|
+
}
|
|
289
|
+
const lines = content.split('\n');
|
|
290
|
+
const sections = [];
|
|
291
|
+
let i = 0;
|
|
292
|
+
while (i < lines.length) {
|
|
293
|
+
const m = lines[i].match(/^##\s+(.+?)\s*$/);
|
|
294
|
+
if (!m) {
|
|
295
|
+
i++;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const title = m[1];
|
|
299
|
+
// 跳过 Plan/Done 段(Plan 有专属 ACTIVE_PLAN_MARKER 重注入;Done 是归档不常驻)
|
|
300
|
+
if (/^Plan:/.test(title) || /^Done:/.test(title)) {
|
|
301
|
+
i++;
|
|
302
|
+
while (i < lines.length && !/^##\s/.test(lines[i]))
|
|
303
|
+
i++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const start = i;
|
|
307
|
+
i++;
|
|
308
|
+
while (i < lines.length && !/^##\s/.test(lines[i]))
|
|
309
|
+
i++;
|
|
310
|
+
const body = lines.slice(start, i).join('\n').trim();
|
|
311
|
+
if (body)
|
|
312
|
+
sections.push({ key: matchSectionKey(title), body });
|
|
313
|
+
}
|
|
314
|
+
// 按优先级降序(优先级高的先占预算)
|
|
315
|
+
sections.sort((a, b) => (SECTION_PRIORITY[b.key] ?? 0) - (SECTION_PRIORITY[a.key] ?? 0));
|
|
316
|
+
let used = 0;
|
|
317
|
+
const out = [];
|
|
318
|
+
for (const s of sections) {
|
|
319
|
+
const remaining = budget - used;
|
|
320
|
+
if (remaining <= 0)
|
|
321
|
+
break;
|
|
322
|
+
const bodyTokens = estimateTokens(s.body);
|
|
323
|
+
if (bodyTokens <= Math.min(remaining, NOTES_PER_SECTION_TOKENS)) {
|
|
324
|
+
out.push(s.body);
|
|
325
|
+
used += bodyTokens;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// 段超预算:段内裁条目(从最近保留)
|
|
329
|
+
const cap = Math.min(remaining, NOTES_PER_SECTION_TOKENS);
|
|
330
|
+
const trimmed = trimSectionToBudget(s.body, cap);
|
|
331
|
+
if (trimmed) {
|
|
332
|
+
out.push(trimmed);
|
|
333
|
+
used += estimateTokens(trimmed);
|
|
334
|
+
}
|
|
335
|
+
// 预算用尽则后续段不再注入正文(降级为索引)
|
|
336
|
+
if (used >= budget)
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
return out.join('\n\n');
|
|
340
|
+
}
|
package/dist/skills/runner.js
CHANGED
|
@@ -144,7 +144,6 @@ const WRITE_TOOLS = new Set(['write_file', 'edit_file', 'run_command']);
|
|
|
144
144
|
* fork 子 agent 模式:`agent:` 显式声明优先;否则按工具面推断——
|
|
145
145
|
* 未声明 allowed-tools(完整工具集)或白名单含写工具 → 'write',纯只读白名单 → 'read'。
|
|
146
146
|
* 避免写类 skill 因缺省字段被静默降级为只读。
|
|
147
|
-
* 导出仅供 scripts/check-skills.ts 离线断言。
|
|
148
147
|
*/
|
|
149
148
|
export function resolveSpawnMode(skill, tools) {
|
|
150
149
|
if (skill.agentMode)
|
|
@@ -14,6 +14,7 @@ import { useSkillTool } from './use-skill.js';
|
|
|
14
14
|
import { runSkillTool } from './run-skill.js';
|
|
15
15
|
import { askHumanTool } from './ask-human.js';
|
|
16
16
|
import { planUpdateTool } from './plan-update.js';
|
|
17
|
+
import { noteAppendTool } from './note-append.js';
|
|
17
18
|
import { memorySaveTool } from './memory-save.js';
|
|
18
19
|
import { memorySearchTool } from './memory-search.js';
|
|
19
20
|
import { memoryListTool } from './memory-list.js';
|
|
@@ -68,6 +69,8 @@ const CAPABILITIES = {
|
|
|
68
69
|
// plan_update 只写内部 notes.md(session 工作面),不作为用户代码 mutation 追踪/回滚/diff;
|
|
69
70
|
// 串行即可(调用不频繁),固定资源键让并发调用排队。
|
|
70
71
|
plan_update: { effect: 'write', concurrency: 'serial', resources: () => ['session-notepad'] },
|
|
72
|
+
// note_append 与 plan_update 同款:只写内部 notes.md 笔记段,不作 project mutation 追踪/diff/回滚;串行 + 固定资源键排队。
|
|
73
|
+
note_append: { effect: 'write', concurrency: 'serial', resources: () => ['session-notepad'] },
|
|
71
74
|
memory_save: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
72
75
|
memory_search: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
73
76
|
memory_list: { effect: 'read', concurrency: 'serial', resources: memoryResource },
|
|
@@ -103,6 +106,7 @@ const rawBuiltinTools = [
|
|
|
103
106
|
runSkillTool,
|
|
104
107
|
askHumanTool,
|
|
105
108
|
planUpdateTool,
|
|
109
|
+
noteAppendTool,
|
|
106
110
|
..._memoryTools,
|
|
107
111
|
subAgentTool,
|
|
108
112
|
];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { appendNoteToSection, NOTE_SECTION_KEYS } from '../../session/notes.js';
|
|
2
|
+
/**
|
|
3
|
+
* note_append:往会话笔记 notes.md 的预设笔记段追加一条 finding/decision/
|
|
4
|
+
* open_question/risk。与 plan_update 的边界:
|
|
5
|
+
* - plan_update 维护执行计划(步骤进度),写 `## Plan:` 段;
|
|
6
|
+
* - note_append 记发现/决策/问题/风险,写 `## Findings` 等笔记段。
|
|
7
|
+
* 写入的笔记正文会由 reinject 注入 system prompt 并常驻(5k token 预算内),
|
|
8
|
+
* compact 后仍可恢复——构成单会话永久记忆。与 memory_* 的边界:
|
|
9
|
+
* - note_append 记本会话内、抗 compact 的笔记;
|
|
10
|
+
* - memory_* 记跨会话稳定事实(另一系统,默认关)。
|
|
11
|
+
*
|
|
12
|
+
* 仿 plan_update:risk=safe,免权限/免 diff/免回滚;capabilities 由 builtins/index.ts
|
|
13
|
+
* 声明为 session-notepad 资源串行(与 plan_update 同款)。
|
|
14
|
+
*/
|
|
15
|
+
function err(message) {
|
|
16
|
+
return { status: 'error', code: 'INVALID_ARGUMENTS', retryable: false, output: `错误:${message}` };
|
|
17
|
+
}
|
|
18
|
+
/** 归一化 section:兼容单复数、下划线/空格/连字符、大小写偏差。 */
|
|
19
|
+
function normalizeSection(raw) {
|
|
20
|
+
const s = String(raw ?? '').trim().toLowerCase().replace(/[-\s]+/g, '_');
|
|
21
|
+
if (NOTE_SECTION_KEYS.includes(s))
|
|
22
|
+
return s;
|
|
23
|
+
// 单数/别名归一
|
|
24
|
+
if (['finding', 'find', 'insight', 'insights'].includes(s))
|
|
25
|
+
return 'findings';
|
|
26
|
+
if (['decision', 'decide', 'choice', 'choices'].includes(s))
|
|
27
|
+
return 'decisions';
|
|
28
|
+
if (['open_question', 'question', 'questions', 'openquestion', 'openquestions'].includes(s))
|
|
29
|
+
return 'open_questions';
|
|
30
|
+
if (['risk', 'hazard', 'caveat', 'caveats'].includes(s))
|
|
31
|
+
return 'risks';
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export const noteAppendTool = {
|
|
35
|
+
name: 'note_append',
|
|
36
|
+
description: 'Append a decision-grade note (a finding, decision, open question, or risk) to the session notepad ' +
|
|
37
|
+
'(`.mocode/sessions/<id>/notes.md`) so it survives context compaction and stays resident in the prompt. ' +
|
|
38
|
+
'Use this for NON-OBVIOUS, lasting-value discoveries — subtle constraints, decisions with downstream impact, ' +
|
|
39
|
+
'open questions that block a choice, or risks that affect later steps. Do NOT use it for routine progress ' +
|
|
40
|
+
'(that is the plan via `plan_update`) or for stable cross-session facts (that is `memory_save`). ' +
|
|
41
|
+
'Notes you write here persist across compaction within this session and are re-injected into the prompt ' +
|
|
42
|
+
'automatically, so the agent keeps remembering what it found/decided. Call it the moment you make the ' +
|
|
43
|
+
'discovery or decision — do not batch to the end.',
|
|
44
|
+
risk: 'safe',
|
|
45
|
+
parameters: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
section: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
enum: NOTE_SECTION_KEYS,
|
|
51
|
+
description: 'Note category: findings (a non-obvious discovery/constraint), decisions (a choice with ' +
|
|
52
|
+
'lasting impact), open_questions (a blocker needing resolution), risks (a hazard affecting later work).',
|
|
53
|
+
},
|
|
54
|
+
entry: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'The note text. One concise, self-contained item: what was found/decided and why it matters. ' +
|
|
57
|
+
'Keep each call to one item — call again for a second item.',
|
|
58
|
+
},
|
|
59
|
+
tag: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: 'Optional short label for grouping (e.g. "parser-bug", "api-shape"). Rendered as **[tag]**.',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ['section', 'entry'],
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
},
|
|
67
|
+
// 兼容模型的 section 命名偏差:单复数/分隔符/别名归一到预设 key。
|
|
68
|
+
normalizeArguments(args) {
|
|
69
|
+
const s = normalizeSection(args.section);
|
|
70
|
+
if (s)
|
|
71
|
+
args.section = s;
|
|
72
|
+
if (typeof args.tag === 'string')
|
|
73
|
+
args.tag = args.tag.trim();
|
|
74
|
+
},
|
|
75
|
+
async execute(args) {
|
|
76
|
+
const section = normalizeSection(args.section);
|
|
77
|
+
if (!section) {
|
|
78
|
+
return err(`section 非法:"${String(args.section ?? '')}"(仅 ${NOTE_SECTION_KEYS.join('/')} 或常见别名)。`);
|
|
79
|
+
}
|
|
80
|
+
const entry = String(args.entry ?? '').trim();
|
|
81
|
+
if (!entry)
|
|
82
|
+
return err('entry 不能为空。');
|
|
83
|
+
if (entry.length > 2000) {
|
|
84
|
+
return err(`entry 过长(${entry.length} 字符,上限 2000)——拆成多条 note_append 或精简。`);
|
|
85
|
+
}
|
|
86
|
+
const tag = typeof args.tag === 'string' && args.tag.trim() ? args.tag.trim() : undefined;
|
|
87
|
+
const result = appendNoteToSection(section, entry, tag);
|
|
88
|
+
if ('error' in result) {
|
|
89
|
+
return { status: 'error', code: 'EXECUTION_ERROR', retryable: false, output: `错误:写入 notes.md 失败: ${result.error}` };
|
|
90
|
+
}
|
|
91
|
+
// note_append 写内部 notes.md,不作为用户代码 mutation 上报 changedFiles(与 plan_update 一致)。
|
|
92
|
+
const titleMap = {
|
|
93
|
+
findings: 'Findings', decisions: 'Decisions', open_questions: 'Open Questions', risks: 'Risks',
|
|
94
|
+
};
|
|
95
|
+
const rendered = tag ? `- **[${tag}]** ${entry}` : `- ${entry}`;
|
|
96
|
+
return {
|
|
97
|
+
status: 'success',
|
|
98
|
+
code: 'OK',
|
|
99
|
+
retryable: false,
|
|
100
|
+
output: `已追加笔记到 ## ${titleMap[section]} 段(将常驻 prompt,抗 compact):\n${rendered}`,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
};
|
package/dist/tools/registry.js
CHANGED
|
@@ -11,6 +11,11 @@ import { isToolErrorOutput } from './result.js';
|
|
|
11
11
|
*/
|
|
12
12
|
const extensions = new Map();
|
|
13
13
|
export const tools = [...builtinTools];
|
|
14
|
+
/** 名字 → 工具 的 O(1) 索引,与 tools 数组在每次 rebuild 时同步重建;findTool 走此索引。 */
|
|
15
|
+
let toolIndex = buildToolIndex(tools);
|
|
16
|
+
function buildToolIndex(list) {
|
|
17
|
+
return new Map(list.map((tool) => [tool.name, tool]));
|
|
18
|
+
}
|
|
14
19
|
export function registerToolsExtension(sourceOrAdditions, maybeAdditions) {
|
|
15
20
|
const source = typeof sourceOrAdditions === 'string' ? sourceOrAdditions : 'external';
|
|
16
21
|
const additions = typeof sourceOrAdditions === 'string' ? (maybeAdditions ?? []) : sourceOrAdditions;
|
|
@@ -35,13 +40,14 @@ function rebuildTools() {
|
|
|
35
40
|
next.push(tool);
|
|
36
41
|
}
|
|
37
42
|
tools.splice(0, tools.length, ...next);
|
|
43
|
+
toolIndex = buildToolIndex(next);
|
|
38
44
|
}
|
|
39
45
|
const DEFAULT_CAPABILITIES = Object.freeze({
|
|
40
46
|
effect: 'unknown',
|
|
41
47
|
concurrency: 'serial',
|
|
42
48
|
});
|
|
43
49
|
export function findTool(name) {
|
|
44
|
-
return
|
|
50
|
+
return toolIndex.get(name);
|
|
45
51
|
}
|
|
46
52
|
/** 缺少声明或找不到工具时返回保守能力,绝不把未知扩展并发执行。 */
|
|
47
53
|
export function getToolCapabilities(toolOrName) {
|
|
@@ -58,10 +64,13 @@ export function getToolResourceKeys(toolOrName, args) {
|
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
/** resource-locked write 是可生成文件 diff/按路径记 rollback 的文件 mutation。 */
|
|
61
|
-
export function
|
|
62
|
-
const capabilities = getToolCapabilities(name);
|
|
67
|
+
export function isFileMutationCapabilities(capabilities) {
|
|
63
68
|
return capabilities.effect === 'write' && capabilities.concurrency === 'resource-locked';
|
|
64
69
|
}
|
|
70
|
+
/** 按工具名判定(兼容入口);热路径请直接复用已解析的 capabilities 走 isFileMutationCapabilities。 */
|
|
71
|
+
export function isFileMutationTool(name) {
|
|
72
|
+
return isFileMutationCapabilities(getToolCapabilities(name));
|
|
73
|
+
}
|
|
65
74
|
function isStructuredOutcome(value) {
|
|
66
75
|
return typeof value === 'object' && value !== null &&
|
|
67
76
|
typeof value.status === 'string' && typeof value.code === 'string' &&
|
|
@@ -144,7 +153,7 @@ async function executeToolOnce(tool, args, signal, opts) {
|
|
|
144
153
|
mutationVersionBefore = mutationBefore.version;
|
|
145
154
|
// Transactional tools own their full write-set capture inside ChangeSet commit.
|
|
146
155
|
const pathCapture = !capabilities.delegatesResourceLocks &&
|
|
147
|
-
|
|
156
|
+
isFileMutationCapabilities(capabilities) && typeof args.path === 'string' && args.path
|
|
148
157
|
? beginPathMutation(args.path)
|
|
149
158
|
: null;
|
|
150
159
|
capturedPath = pathCapture?.path;
|
package/dist/ui/diff.js
CHANGED
|
@@ -254,7 +254,7 @@ export function renderFileChange(opts) {
|
|
|
254
254
|
/** 头行 + 计数行 + 正文(折叠 + 截断 + 行号),每行尾 \n。 */
|
|
255
255
|
function renderBody(head, counts, items, padW, startLine, lang) {
|
|
256
256
|
const lines = [head, counts];
|
|
257
|
-
const metaPrefix = `${BODY_INDENT}
|
|
257
|
+
const metaPrefix = `${BODY_INDENT}`; // 与正文行左对齐(batch 展开时还会再加外层 indent,避免双重缩进导致提示行突兀)
|
|
258
258
|
let oldLine = startLine;
|
|
259
259
|
let newLine = startLine;
|
|
260
260
|
let shown = 0;
|
package/dist/ui/layout.js
CHANGED
|
@@ -1936,7 +1936,7 @@ function renderDimInputRow(prompt, text, placeholder, cols) {
|
|
|
1936
1936
|
/**
|
|
1937
1937
|
* 单行运行态滑窗:以光标为中心,向左右扩展填满 contentW-1(留 1 cell 给光标),
|
|
1938
1938
|
* 返回可见子串与光标在子串内的显示列。保证光标恒可见,且不软折行(运行态输入框恒单行,
|
|
1939
|
-
* 不触发 setRegion/ED
|
|
1939
|
+
* 不触发 setRegion/ED,避免流式期间底栏抖动)。
|
|
1940
1940
|
*/
|
|
1941
1941
|
function windowSingleLine(text, cursor, contentW) {
|
|
1942
1942
|
const n = text.length;
|
package/dist/ui/render.js
CHANGED
|
@@ -2,7 +2,13 @@ import { stdout } from 'node:process';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { ui } from './theme.js';
|
|
4
4
|
import { t } from '../i18n/index.js';
|
|
5
|
-
|
|
5
|
+
let VERSION = '0.0.0';
|
|
6
|
+
try {
|
|
7
|
+
VERSION = createRequire(import.meta.url)('../../package.json').version;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// 非标准布局(测试编译产物、自定义打包)不含 package.json:版本号回落,不阻断模块加载。
|
|
11
|
+
}
|
|
6
12
|
/**
|
|
7
13
|
* 清空整屏 + 滚动缓冲(向上滚动可见的历史输出),光标归位。
|
|
8
14
|
* 进入会话时调用,让终端只剩当前 agent 对话。非 TTY 时空操作。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "tsx src/index.ts",
|
|
23
23
|
"build": "tsc -p tsconfig.build.json",
|
|
24
|
-
"
|
|
24
|
+
"test": "tsc -p tsconfig.test-build.json && node --test --experimental-test-isolation=none \"dist-tests/tests/*.test.js\"",
|
|
25
|
+
"typecheck": "tsc --noEmit && tsc -p tests/tsconfig.json && tsc -p evals/tsconfig.json",
|
|
25
26
|
"eval:smoke": "tsx evals/smoke.ts && tsx evals/coding/smoke.ts",
|
|
26
27
|
"eval:coding": "tsx evals/coding/runner.ts",
|
|
27
28
|
"eval:coding:list": "tsx evals/coding/runner.ts --list",
|