mocode-ai 1.0.13 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -19
- package/README.zh-CN.md +1 -1
- package/bin/mocode-agent-host.js +3 -0
- package/dist/agent/core.js +10 -5
- package/dist/agent/mode.js +9 -10
- package/dist/agent/spawn.js +2 -2
- package/dist/config/index.js +43 -65
- package/dist/context/artifacts.js +2 -2
- package/dist/context/classifier.js +3 -6
- package/dist/context/encoders/_util.js +2 -1
- package/dist/context/encoders/index.js +3 -4
- package/dist/context/lifecycle.js +3 -23
- package/dist/context/relevance.js +3 -22
- package/dist/host/protocol.js +40 -0
- package/dist/host/stdio.js +203 -0
- package/dist/i18n/index.js +0 -30
- package/dist/memory/index.js +6 -0
- package/dist/repl/index.js +32 -284
- package/dist/sandbox/policy.js +6 -4
- package/dist/skills/builtin-skills.js +56 -0
- package/dist/skills/discover.js +7 -0
- package/dist/skills/index.js +23 -4
- package/dist/tools/builtins/edit-file.js +68 -14
- package/dist/tools/builtins/glob.js +1 -1
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +0 -14
- package/dist/tools/builtins/read-file.js +1 -1
- package/dist/tools/constants.js +2 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
|
|
|
72
72
|
- **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.
|
|
73
73
|
- **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).
|
|
74
74
|
- **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.
|
|
75
|
-
- **Project context (
|
|
75
|
+
- **Project context (`MOCODE.md`)** — A single project-level memory file at `MOCODE.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.
|
|
76
76
|
- **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 can record intermediate findings, design decisions, open questions, and structured plans. A live progress chip in the TUI status bar shows `plan: [title] (3/7) ▸ [current step]` when a `## Plan:` section is present. The agent manages the file directly with write_file/edit_file/read_file.
|
|
77
77
|
- **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.
|
|
78
78
|
- **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.
|
|
@@ -89,7 +89,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
|
|
|
89
89
|
## Documentation
|
|
90
90
|
|
|
91
91
|
- [中文使用指南](./docs/usage.md) — 菜单式快速上手、命令速查、模式、会话、项目上下文与排障。
|
|
92
|
-
- [Project context](./docs/
|
|
92
|
+
- [Project context](./docs/usage.md#项目上下文) — `MOCODE.md` and Skills.
|
|
93
93
|
|
|
94
94
|
## Installation
|
|
95
95
|
|
|
@@ -235,8 +235,6 @@ The five `memory_*` tools are gated on `MEMORY_ENABLED=true` at startup; toggle
|
|
|
235
235
|
| `/pet` | Toggle the optional desktop pet (floating window mirroring agent state) |
|
|
236
236
|
| `/pet skin` | Pick a pet skin (↑↓ · Enter) |
|
|
237
237
|
| `/pet quit` | Fully shut down the pet process (not just disconnect) |
|
|
238
|
-
| `/snapshot_refresh` | Refresh project snapshot (re-scan files + regenerate LLM summary) |
|
|
239
|
-
| `/snapshot` | Toggle project snapshot on/off |
|
|
240
238
|
|
|
241
239
|
Type `/` to trigger the dropdown menu, keep typing to filter; Esc to cancel.
|
|
242
240
|
|
|
@@ -264,21 +262,6 @@ MoCode automatically scans the following directories for skills (each skill is a
|
|
|
264
262
|
|
|
265
263
|
A skill's `description` is injected into the system prompt (progressive disclosure, tier 1); the model calls `use_skill` to load the full body (tier 2) only when the task is relevant. Use `/skills` to see discovered skills.
|
|
266
264
|
|
|
267
|
-
## Project Context (Snapshot + Skill)
|
|
268
|
-
|
|
269
|
-
MoCode uses two complementary systems to help the agent understand your project:
|
|
270
|
-
|
|
271
|
-
- **Project Snapshot** (automatic + LLM-enhanced, enabled by default): Scans your project files and generates a structured summary including project description, tech stack, key commands, module responsibilities, and directory tree. Stored in `.mocode/snapshot.json` and reused across sessions. Use `/snapshot` to toggle it and `/snapshot_refresh` after major changes.
|
|
272
|
-
- **Project Skill** (manual + AI-assisted, disabled by default): A knowledge base for design decisions, architectural patterns, pitfalls, conventions, and workflow notes. Enable it with `/project_skill on` (or `MOCODE_PROJECT_SKILL=true`), generate an initial draft with `/project_skill init`, then refine `.mocode/project-skill.md`. The agent can update it through `project_skill_update` during conversations.
|
|
273
|
-
|
|
274
|
-
**Complementary principle**: Snapshot provides *what/where* (facts: files, structure, commands), Skill provides *why/how* (insights: decisions, behaviors, gotchas). No duplication, ~46% token savings compared to the previous approach.
|
|
275
|
-
|
|
276
|
-
Control via environment variables:
|
|
277
|
-
- `MOCODE_PROJECT_SNAPSHOT=false` — disable snapshot
|
|
278
|
-
- `MOCODE_PROJECT_SKILL=true` — enable Project Skill
|
|
279
|
-
|
|
280
|
-
See [docs/USAGE_SNAPSHOT_SKILL.md](./docs/USAGE_SNAPSHOT_SKILL.md) for detailed usage.
|
|
281
|
-
|
|
282
265
|
## Working discipline (Build-and-Self-Verify)
|
|
283
266
|
|
|
284
267
|
Every coding task runs through four sequential phases. Skipping or merging them is a failure mode — the system prompt injects this discipline on every turn (see `src/agent/work-discipline.ts`):
|
package/README.zh-CN.md
CHANGED
package/dist/agent/core.js
CHANGED
|
@@ -165,7 +165,7 @@ function readDiffContext(tc, parsed) {
|
|
|
165
165
|
* - pruner 在每个 runAgentCore 实例化一次(本闭包持有),会话级状态。
|
|
166
166
|
* - 开关关闭时 pruner 不创建(零开销、零行为变化)。
|
|
167
167
|
* 出口再经 Lifecycle Engine 做引用追踪:LIVE→REFERENCED→OBSOLETE→STUB 四态。
|
|
168
|
-
* - lifecycle 也在每个 runAgentCore 实例化一次,登记 grep/glob/
|
|
168
|
+
* - lifecycle 也在每个 runAgentCore 实例化一次,登记 grep/glob/web_search/web_fetch 等 producer
|
|
169
169
|
* 与 read/edit/write 的 consumer 关系;孤立+老化自动 STUB(观察类工具永不到 STUB)。
|
|
170
170
|
* - 开关关闭时 lifecycle=null 完全跳过。 */
|
|
171
171
|
/** RETRY-01: 把 thrash hint + 反思指针一次性拼到 output 尾部。
|
|
@@ -250,7 +250,8 @@ export async function runAgentCore(opts) {
|
|
|
250
250
|
? null
|
|
251
251
|
: (opts.preCompletionChecklist ?? _checklistMiddleware.handler);
|
|
252
252
|
const maxSteps = opts.maxSteps ?? config.maxSteps;
|
|
253
|
-
// 中断还原:
|
|
253
|
+
// 中断还原:repl 的 /plan / /auto / Shift+Tab 等用户面触发 setAgentMode 中途切了模式,
|
|
254
|
+
// abort 时连同模式一起还原回轮首。模型不再持有 switch_mode 工具,无法自切。
|
|
254
255
|
const savedMode = getAgentMode();
|
|
255
256
|
// 本轮计时:从入口到完毕(正常 return / 达上限),供 finally 打 ✻ Worked for 摘要行。
|
|
256
257
|
const t0 = Date.now();
|
|
@@ -318,7 +319,7 @@ export async function runAgentCore(opts) {
|
|
|
318
319
|
// 相关性裁剪 pruner:每个 runAgentCore 实例一个,纯静态、不调 LLM、自动判定 read_file 失效。
|
|
319
320
|
// 开关关闭时为 null,所有 pushToolResult 调用走无 pruner 路径(零行为变化)。
|
|
320
321
|
const relprune = config.contextRelprune ? createRelevancePruner() : null;
|
|
321
|
-
// 观察者生命周期引擎:每个 runAgentCore 实例一个,纯静态、自动维护 grep/glob/
|
|
322
|
+
// 观察者生命周期引擎:每个 runAgentCore 实例一个,纯静态、自动维护 grep/glob/web_search/web_fetch 等
|
|
322
323
|
// producer 与 read/edit/write 的 consumer 引用关系;孤立+老化的非观察类工具自动 STUB。
|
|
323
324
|
// 开关关闭时为 null,所有 pushToolResult / mutation 调用走无 lifecycle 路径(零行为变化)。
|
|
324
325
|
// 引擎需要从已有会话 history 恢复观察结果的年龄和 path 索引;不能只追踪本次
|
|
@@ -693,7 +694,9 @@ export async function runAgentCore(opts) {
|
|
|
693
694
|
: false;
|
|
694
695
|
let denied;
|
|
695
696
|
if (tool && argumentsValid) {
|
|
696
|
-
const perm = await checkPermission(tool, parsed ?? {}, signal
|
|
697
|
+
const perm = await checkPermission(tool, parsed ?? {}, signal, {
|
|
698
|
+
prompt: opts.permissionPrompt,
|
|
699
|
+
});
|
|
697
700
|
emitTrace('permission', {
|
|
698
701
|
source: 'agent_tool',
|
|
699
702
|
tool: tc.name,
|
|
@@ -785,7 +788,9 @@ export async function runAgentCore(opts) {
|
|
|
785
788
|
? validateToolArguments(tool, parsed).valid
|
|
786
789
|
: false;
|
|
787
790
|
if (tool && argumentsValid) {
|
|
788
|
-
const perm = await checkPermission(tool, parsed ?? {}, signal
|
|
791
|
+
const perm = await checkPermission(tool, parsed ?? {}, signal, {
|
|
792
|
+
prompt: opts.permissionPrompt,
|
|
793
|
+
});
|
|
789
794
|
emitTrace('permission', {
|
|
790
795
|
source: 'agent_tool',
|
|
791
796
|
tool: tc.name,
|
package/dist/agent/mode.js
CHANGED
|
@@ -2,19 +2,18 @@
|
|
|
2
2
|
* agent 模式的共享状态(零依赖纯叶子)。
|
|
3
3
|
*
|
|
4
4
|
* 把 `agentMode` 从 repl 的模块变量提到这里,让三方都能用、且不破坏依赖单向:
|
|
5
|
-
* - `
|
|
5
|
+
* - `repl/index.ts` 读写它 + 注册 onModeChange 监听器(做 applyMode 重写 history[0] +
|
|
6
|
+
* refreshStatusBase 刷状态行 modeTag),并通过 /plan / /auto / Shift+Tab(c cycleMode) 触发切换。
|
|
6
7
|
* - `agent/index.ts` 每步读它(getAgentMode)——决定 chat() 用全量 chatTools 还是 planChatTools 只读子集,
|
|
7
|
-
* 以及串行分支的 plan 防御 backstop
|
|
8
|
-
* -
|
|
9
|
-
* 刷状态行 modeTag)。
|
|
8
|
+
* 以及串行分支的 plan 防御 backstop。这样切换模式后,下一次 chat() 立即看到新工具集。
|
|
9
|
+
* - 模型不再持有 switch_mode 工具(已砍):模式切换只能由用户面触发。
|
|
10
10
|
*
|
|
11
|
-
* 依赖方向无环:本模块不 import
|
|
12
|
-
* → agent/mode` 链终点是本叶子,不回到 agent/index。`tools → agent/mode` 与既有 `tools → ui`(ask-human)、
|
|
13
|
-
* `tools → rollback`(registry)同性质——都是引叶子。
|
|
11
|
+
* 依赖方向无环:本模块不 import 任何业务模块。
|
|
14
12
|
*
|
|
15
|
-
* listener 同步触发:setAgentMode 在变更模式后同步调 listener(无 async)。
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* listener 同步触发:setAgentMode 在变更模式后同步调 listener(无 async)。repl 的
|
|
14
|
+
* cycleMode / /plan / /auto 在 executeTool 路径之外调 setAgentMode → listener 同步
|
|
15
|
+
* applyMode(重写 history[0])+ refreshStatusBase;切换后下一步 chat() 读到新模式 +
|
|
16
|
+
* 新系统提示,一致。
|
|
18
17
|
*/
|
|
19
18
|
let currentMode = 'auto';
|
|
20
19
|
let listener = null;
|
package/dist/agent/spawn.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function spawnAgent(opts) {
|
|
|
66
66
|
let toolsOverride;
|
|
67
67
|
const mode = opts.mode ?? 'read';
|
|
68
68
|
const requested = opts.tools?.length ? new Set(opts.tools) : null;
|
|
69
|
-
const readOnly = new Set(['read_file', 'glob', 'grep', '
|
|
69
|
+
const readOnly = new Set(['read_file', 'glob', 'grep', 'web_search', 'web_fetch', 'use_skill', 'memory_search', 'memory_list']);
|
|
70
70
|
toolsOverride = chatTools.filter((tool) => tool.function.name !== 'sub-agent' &&
|
|
71
71
|
(!requested || requested.has(tool.function.name)) &&
|
|
72
72
|
(mode === 'write' || readOnly.has(tool.function.name)));
|
|
@@ -139,7 +139,7 @@ export async function spawnAgent(opts) {
|
|
|
139
139
|
onToolOutcome: (tool, args) => {
|
|
140
140
|
if (tool === 'read_file' && typeof args.path === 'string')
|
|
141
141
|
readSet.add(args.path);
|
|
142
|
-
else if (['glob', 'grep'
|
|
142
|
+
else if (['glob', 'grep'].includes(tool))
|
|
143
143
|
readSet.add('workspace');
|
|
144
144
|
},
|
|
145
145
|
});
|
package/dist/config/index.js
CHANGED
|
@@ -2,8 +2,6 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
|
-
import { loadSnapshot } from '../project-snapshot/index.js';
|
|
6
|
-
import { buildProjectSkillSection } from '../project-skill/index.js';
|
|
7
5
|
import { getSandboxRoot } from '../sandbox/root.js';
|
|
8
6
|
import { getCurrentSessionId } from '../session/state.js';
|
|
9
7
|
import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
|
|
@@ -85,30 +83,6 @@ const PLATFORM_NOTE = (() => {
|
|
|
85
83
|
* 默认关(新用户零侵入):这段 + 工具表里的 5 个 memory_* + 系统提示尾部的 Memory Index
|
|
86
84
|
* 都不出现;打开 /memory_switch 后下一次新建 system message 才注入。
|
|
87
85
|
*/
|
|
88
|
-
/**
|
|
89
|
-
* 项目快照段落:开 isProjectSnapshotEnabled() 且有快照时才拼。
|
|
90
|
-
* 告诉 LLM 项目已有哪些静态文件可 cache hit,减少无谓 read_file。
|
|
91
|
-
*/
|
|
92
|
-
function buildSnapshotSection() {
|
|
93
|
-
if (!config.projectSnapshotEnabled)
|
|
94
|
-
return '';
|
|
95
|
-
try {
|
|
96
|
-
const snap = loadSnapshot();
|
|
97
|
-
if (!snap)
|
|
98
|
-
return '';
|
|
99
|
-
// 快照内容已经是完整的 markdown,直接返回
|
|
100
|
-
return `\n${snap.content}\n`;
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return '';
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* 项目快照总开关查询器(read-file 工具用)。
|
|
108
|
-
*/
|
|
109
|
-
export function isProjectSnapshotEnabled() {
|
|
110
|
-
return config.projectSnapshotEnabled;
|
|
111
|
-
}
|
|
112
86
|
/**
|
|
113
87
|
* Session notepad 段落:读取 .mocode/sessions/<sessionId>/notes.md,只注入 ## 标题行作为目录摘要。
|
|
114
88
|
* Agent 用 write_file/edit_file/read_file 维护此文件,抗 compact(在 context window 之外)。
|
|
@@ -165,23 +139,31 @@ const SYSTEM_PROMPT_MEMORY_SECTION = `
|
|
|
165
139
|
* memoryEnabled=false 时:memory_save/update/forget 三个写工具名字 + "memory-write tools" 这
|
|
166
140
|
* 句都不出现,且 read-only 列表里的 memory_search/memory_list 也移除——避免提示词里出现
|
|
167
141
|
* 根本不存在的工具名引起 LLM 调不到。
|
|
142
|
+
*
|
|
143
|
+
* .codegraph/ 存在性也动态决定:有索引时引导 LLM 优先用 codegraph skill(免去逐文件扫);
|
|
144
|
+
* 没索引时干脆不提,避免 LLM 调出失败。函数化(非 const)以便在 buildPlanModeSuffix 里现拼。
|
|
168
145
|
*/
|
|
169
|
-
|
|
170
|
-
|
|
146
|
+
function buildPlanResearchRules() {
|
|
147
|
+
const cg = hasCodegraphIndex()
|
|
148
|
+
? ' If `.codegraph/` exists, prefer loading the `codegraph` skill (via use_skill) and using run_command to query it.'
|
|
149
|
+
: '';
|
|
150
|
+
return `
|
|
151
|
+
- Research enough to locate relevant code, trace call paths, and understand existing conventions.${cg} Otherwise rely on read_file / glob / grep. Do not repeat information already retrieved in this session.
|
|
171
152
|
- Produce an actionable plan: files and reasons, ordered steps, edge cases, and verification (typecheck / tests / build).
|
|
172
153
|
- When ready, MUST call the \`ask_human\` tool with a concise summary and exactly these options:
|
|
173
|
-
1. "按计划执行 (switch to auto and
|
|
154
|
+
1. "按计划执行 (please run /auto to switch to auto mode and proceed)" — wait for the user to run /auto, then implement.
|
|
174
155
|
2. "继续细化方案 (stay in plan, refine)" — remain in plan and refine.
|
|
175
156
|
3. "取消 / 暂不执行 (abort)" — stop without switching mode.
|
|
176
157
|
- Never silently switch or stop. Do not ask approval in plain text; \`ask_human\` is the approval channel.
|
|
177
158
|
- The REPL approval prompt is only a fallback; do not rely on it.`;
|
|
159
|
+
}
|
|
178
160
|
function buildPlanModeSuffix() {
|
|
179
161
|
const memoryTools = isMemoryEnabled()
|
|
180
162
|
? 'memory_save, memory_update, memory_forget'
|
|
181
163
|
: '';
|
|
182
164
|
const readOnlyTools = isMemoryEnabled()
|
|
183
|
-
? 'read_file, glob, grep,
|
|
184
|
-
: 'read_file, glob, grep,
|
|
165
|
+
? 'read_file, glob, grep, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list'
|
|
166
|
+
: 'read_file, glob, grep, web_search, web_fetch, use_skill, ask_human';
|
|
185
167
|
const removed = ['write_file', 'edit_file', 'run_command', memoryTools]
|
|
186
168
|
.filter(Boolean)
|
|
187
169
|
.join(', ');
|
|
@@ -190,7 +172,7 @@ function buildPlanModeSuffix() {
|
|
|
190
172
|
## ⛯ PLAN MODE (active now)
|
|
191
173
|
You are in PLAN mode: investigate and design only — do NOT execute or change anything.
|
|
192
174
|
- Removed from your tool list: ${removed}. Use only these read-only tools: ${readOnlyTools}.
|
|
193
|
-
${
|
|
175
|
+
${buildPlanResearchRules()}`;
|
|
194
176
|
}
|
|
195
177
|
/** 兼容旧名字:repl 的 buildSystemMessage 仍引 PLAN_MODE_SUFFIX(变量)。运行时按需现拼。 */
|
|
196
178
|
export function buildBasePrompt(sessionId = getCurrentSessionId()) {
|
|
@@ -225,12 +207,12 @@ ${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
|
225
207
|
|
|
226
208
|
## Workflow
|
|
227
209
|
- Understand requirements and current code before acting; do not guess.
|
|
228
|
-
- If \`.codegraph/\` exists, use \`codegraph\` first for unfamiliar code questions. Use direct reads for known or recently changed files.
|
|
229
210
|
- After modifications, run the smallest relevant verification, then typecheck/build when appropriate. Never claim success without evidence.
|
|
230
211
|
- Use web search only when freshness materially affects the answer (new APIs, versions, security, current UI conventions).
|
|
212
|
+
${buildCodegraphSection()}
|
|
231
213
|
|
|
232
214
|
## Tool rules
|
|
233
|
-
- Precise path/symbol → go directly to \`read_file
|
|
215
|
+
- Precise path/symbol → go directly to \`read_file\`; use \`glob\`/\`grep\` only for discovery. If a codegraph index is available (see Workflow above), prefer \`codegraph node\` / \`codegraph query\` for symbol lookups.
|
|
234
216
|
- Before editing, read the exact target region and copy both its artifact \`hash\` and verbatim text. Use \`edit_file\` with \`expected_hash\` for unique local replacements, and \`write_file\` with the latest hash for replacement (or null only for creation).
|
|
235
217
|
- Local edits require an exact unique match; use \`write_file\` for new/full files.
|
|
236
218
|
- Use \`glob\`/\`grep\` for discovery and \`run_command\` for execution or verification, not file existence checks. State intent before side effects.
|
|
@@ -254,7 +236,7 @@ ${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
|
254
236
|
- Operate only within authorized scope; when unsure, ask — don't guess.
|
|
255
237
|
|
|
256
238
|
## Project context (dynamic reference)
|
|
257
|
-
${
|
|
239
|
+
${memorySection}${buildNotepadSection(sessionId)}
|
|
258
240
|
|
|
259
241
|
## Session Notepad — working notes file
|
|
260
242
|
${sessionId
|
|
@@ -394,10 +376,8 @@ export const config = {
|
|
|
394
376
|
theme: process.env.MOCODE_THEME || 'default',
|
|
395
377
|
themeFromShell,
|
|
396
378
|
llmKeysFromShell,
|
|
397
|
-
projectSnapshotEnabled: process.env.MOCODE_PROJECT_SNAPSHOT !== 'false',
|
|
398
379
|
permissionEnabled: process.env.MOCODE_PERMISSION !== 'false',
|
|
399
380
|
permissionNonInteractiveAllow: process.env.MOCODE_PERMISSION_NON_INTERACTIVE_ALLOW === 'true',
|
|
400
|
-
projectSkillEnabled: process.env.MOCODE_PROJECT_SKILL === 'true',
|
|
401
381
|
};
|
|
402
382
|
/**
|
|
403
383
|
* 运行时更新模型相关配置(/model 命令调)。
|
|
@@ -442,6 +422,32 @@ export function updateSubAgentConfig(enabled) {
|
|
|
442
422
|
export function isMemoryEnabled() {
|
|
443
423
|
return config.memoryEnabled;
|
|
444
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* .codegraph/ 索引存在性:仅查 cwd 顶层 .codegraph(目录或文件均可,codegraph CLI
|
|
427
|
+
* 自己会处理内部布局)。用于动态决定是否在系统提示里注入 codegraph skill 用法段——
|
|
428
|
+
* 没有索引的项目不应被提示「用 codegraph」以免 LLM 调出失败。失败静默返 false。
|
|
429
|
+
*/
|
|
430
|
+
export function hasCodegraphIndex() {
|
|
431
|
+
try {
|
|
432
|
+
return fs.existsSync(path.join(process.cwd(), '.codegraph'));
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* 当 .codegraph/ 存在时拼进 auto 模式系统提示的 codegraph 段;否则返空串(零成本)。
|
|
440
|
+
* 单一来源:被 buildBasePrompt 注入,确保 basePrompt 不含死字符串。
|
|
441
|
+
*/
|
|
442
|
+
export function buildCodegraphSection() {
|
|
443
|
+
if (!hasCodegraphIndex())
|
|
444
|
+
return '';
|
|
445
|
+
return [
|
|
446
|
+
'',
|
|
447
|
+
'## Codegraph (project has .codegraph/ index)',
|
|
448
|
+
'- For unfamiliar code questions, prefer loading the `codegraph` skill (via use_skill) and querying it with run_command (`codegraph explore <entry>`, `codegraph node <symbol>`). Falls back to read_file / glob / grep when not applicable.',
|
|
449
|
+
].join('\n');
|
|
450
|
+
}
|
|
445
451
|
/**
|
|
446
452
|
* 切换记忆子系统开关(/memory_switch on|off 调)。
|
|
447
453
|
* - 更新 config 单例字段(其它模块下次调 isMemoryEnabled() 即拿新值)。
|
|
@@ -457,34 +463,6 @@ export function updateMemoryConfig(enabled) {
|
|
|
457
463
|
config.memoryEnabled = enabled;
|
|
458
464
|
process.env.MEMORY_ENABLED = enabled ? 'true' : 'false';
|
|
459
465
|
}
|
|
460
|
-
/**
|
|
461
|
-
* 项目专属 Skill 总开关:单一来源。/project_skill、buildBasePrompt、
|
|
462
|
-
* tools/builtins/index.ts 都从这里查。默认 false(零侵入)。
|
|
463
|
-
*/
|
|
464
|
-
export function isProjectSkillEnabled() {
|
|
465
|
-
return config.projectSkillEnabled;
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* 切换项目专属 Skill 开关(/project_skill on|off 调)。
|
|
469
|
-
* - 更新 config 单例字段(其它模块下次调 isProjectSkillEnabled() 即拿新值)。
|
|
470
|
-
* - 同步 process.env.MOCODE_PROJECT_SKILL(下次启动 loadEnvFiles 不被文件回填)。
|
|
471
|
-
* 持久化(写 ~/.mocode/config 的 MOCODE_PROJECT_SKILL 键)由调用方走 writeConfigKeys。
|
|
472
|
-
*/
|
|
473
|
-
export function updateProjectSkillConfig(enabled) {
|
|
474
|
-
config.projectSkillEnabled = enabled;
|
|
475
|
-
process.env.MOCODE_PROJECT_SKILL = enabled ? 'true' : 'false';
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* 切换项目快照开关(/snapshot on|off 调)。
|
|
479
|
-
* - 更新 config 单例字段(其它模块下次读 config.projectSnapshotEnabled 即拿新值:
|
|
480
|
-
* buildSnapshotSection 现拼现读、read-file 每次 execute 现读)。
|
|
481
|
-
* - 同步 process.env.MOCODE_PROJECT_SNAPSHOT(下次启动 loadEnvFiles 不会被文件回填)。
|
|
482
|
-
* 持久化(写 ~/.mocode/config 的 MOCODE_PROJECT_SNAPSHOT 键)由调用方走 updateConfigKey。
|
|
483
|
-
*/
|
|
484
|
-
export function updateSnapshotConfig(enabled) {
|
|
485
|
-
config.projectSnapshotEnabled = enabled;
|
|
486
|
-
process.env.MOCODE_PROJECT_SNAPSHOT = enabled ? 'true' : 'false';
|
|
487
|
-
}
|
|
488
466
|
/** 切换界面与模型回复语言;持久化由 REPL 调用 config/file.ts 完成。 */
|
|
489
467
|
export function updateLanguageConfig(language) {
|
|
490
468
|
setLanguage(language);
|
|
@@ -40,7 +40,7 @@ function callArgs(history, idx) {
|
|
|
40
40
|
function sourceType(tool) {
|
|
41
41
|
if (tool === 'read_file')
|
|
42
42
|
return 'read';
|
|
43
|
-
if (tool === 'grep' || tool === 'glob'
|
|
43
|
+
if (tool === 'grep' || tool === 'glob')
|
|
44
44
|
return 'search';
|
|
45
45
|
if (tool === 'run_command')
|
|
46
46
|
return 'diagnostic';
|
|
@@ -53,7 +53,7 @@ function pathsFromOutput(tool, output) {
|
|
|
53
53
|
if (normalized)
|
|
54
54
|
paths.add(normalized);
|
|
55
55
|
};
|
|
56
|
-
if (tool === 'grep' || tool === '
|
|
56
|
+
if (tool === 'grep' || tool === 'run_command') {
|
|
57
57
|
const expression = /^(.+?\.[A-Za-z0-9]+):(?:\d+|\s*\d+\s*(?:处匹配|matches?))/gmi;
|
|
58
58
|
let match;
|
|
59
59
|
while ((match = expression.exec(output)))
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
// Context Classifier:据工具名(强先验)+ 输出形状(启发)+ 兜底,选 ContextKind。
|
|
2
2
|
//
|
|
3
3
|
// 三级信号:
|
|
4
|
-
// 1) 名字强先验(BY_NAME
|
|
4
|
+
// 1) 名字强先验(BY_NAME 表,覆盖全部内置工具,确定性强)。
|
|
5
5
|
// 2) 形状启发(为 MCP 工具 / 未来工具 / 未登记工具兜底识别)。
|
|
6
6
|
// 3) 兜底 'passthrough'(不认识 = 不动,零行为变化)。
|
|
7
7
|
//
|
|
8
8
|
// 单一事实源风格(仿 tools/constants.ts 的 READ_TOOL_NAMES / PLAN_DISABLED_TOOLS)。
|
|
9
9
|
// 加新工具:在 BY_NAME 加一行;或靠形状启发自动识别。
|
|
10
|
-
/** 工具名 → ContextKind 的强先验表(
|
|
10
|
+
/** 工具名 → ContextKind 的强先验表(覆盖全部内置工具)。 */
|
|
11
11
|
const BY_NAME = {
|
|
12
12
|
// tree:路径列表 → 缩进树
|
|
13
13
|
glob: 'tree',
|
|
14
14
|
// search:file:line 分组
|
|
15
15
|
grep: 'search',
|
|
16
16
|
web_search: 'search',
|
|
17
|
-
// graph:CLI dump → 精炼图
|
|
18
|
-
codegraph: 'graph',
|
|
19
17
|
// log:分级 / 折叠 / 尾偏置
|
|
20
18
|
run_command: 'log',
|
|
21
19
|
// code:保行号(edit_file 依赖,最敏感)
|
|
@@ -31,7 +29,6 @@ const BY_NAME = {
|
|
|
31
29
|
edit_file: 'status',
|
|
32
30
|
write_file: 'status',
|
|
33
31
|
ask_human: 'status',
|
|
34
|
-
switch_mode: 'status',
|
|
35
32
|
drop_context: 'status',
|
|
36
33
|
memory_save: 'status',
|
|
37
34
|
memory_update: 'status',
|
|
@@ -47,7 +44,7 @@ function classifyByShape(output) {
|
|
|
47
44
|
// file:line: content 形(grep 风格)
|
|
48
45
|
if (/^[^\n:]+:\d+:[^\n]*$/m.test(output))
|
|
49
46
|
return 'search';
|
|
50
|
-
// [退出码 N] 前缀(run_command
|
|
47
|
+
// [退出码 N] 前缀(run_command 风格)
|
|
51
48
|
if (/^\[退出码 \d+\]/m.test(output))
|
|
52
49
|
return 'log';
|
|
53
50
|
// 路径列表:多行都是含分隔符的相对路径(glob 风格)
|
|
@@ -6,8 +6,9 @@ export function stripAnsi(s) {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* 折叠连续空行(只含空白字符的行)≥ threshold → 单个空行。
|
|
9
|
-
* 用于 prose / source dump(web_fetch / use_skill / task
|
|
9
|
+
* 用于 prose / source dump(web_fetch / use_skill / task):HTML→文本与 markdown 常留多余空行,
|
|
10
10
|
* 多空行与单空行语义等价,折叠无损。默认 threshold=3(只动真正过量的空行,常见 ≤2 空行不动)。
|
|
11
|
+
* 注:codegraph 已 skill 化(用 run_command 调 CLI,落到 log encoder),graph kind 现仅作保留值。
|
|
11
12
|
*
|
|
12
13
|
* 注意:用 `.trim() === ''` 判空——故 read_file 的 ` 2\t`(行号前缀 + tab,trim 后剩 `2`)不会被
|
|
13
14
|
* 视作空行。read_file 的空行折叠见 code encoder(前缀感知)。
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// Phase 2:tree / search / log / table / memory(高价值低风险)。
|
|
5
5
|
// Phase 2.5:code / graph / doc / summary(覆盖剩余有 encoder 的 kind)。
|
|
6
6
|
// - code(read_file):仅折叠 ≥3 连续空行,行号保真(edit_file 依赖)。
|
|
7
|
-
// - graph(
|
|
8
|
-
//
|
|
7
|
+
// - graph(保留 kind,暂无 builtin 工具直接命中)/ doc(web_fetch, use_skill)/ summary(task):
|
|
8
|
+
// 去 ANSI + 折叠空行,保守不重构结构。
|
|
9
|
+
// - status(edit/write/ask_human/mem 增删改)无 encoder:本就是一行,无需编码。
|
|
9
10
|
//
|
|
10
11
|
// 加 encoder:新建 encoders/xxx.ts 导出 ContextEncoder,在此数组加一行。无需动 agent / llm / core。
|
|
11
12
|
import { passthroughEncoder } from './passthrough.js';
|
|
@@ -15,7 +16,6 @@ import { commandEncoder } from './command.js';
|
|
|
15
16
|
import { tableEncoder } from './table.js';
|
|
16
17
|
import { memoryEncoder } from './memory.js';
|
|
17
18
|
import { codeEncoder } from './code.js';
|
|
18
|
-
import { graphEncoder } from './graph.js';
|
|
19
19
|
import { docEncoder } from './doc.js';
|
|
20
20
|
import { summaryEncoder } from './summary.js';
|
|
21
21
|
export const builtinEncoders = [
|
|
@@ -26,7 +26,6 @@ export const builtinEncoders = [
|
|
|
26
26
|
tableEncoder,
|
|
27
27
|
memoryEncoder,
|
|
28
28
|
codeEncoder,
|
|
29
|
-
graphEncoder,
|
|
30
29
|
docEncoder,
|
|
31
30
|
summaryEncoder,
|
|
32
31
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Observation Lifecycle Engine:tool 消息的「观察者生命周期」状态机。
|
|
2
2
|
//
|
|
3
3
|
// 在 Relevance Pruner 之上的第二层被动裁剪。Relevance Pruner 只管 read_file 的「同 path 新旧替换 +
|
|
4
|
-
// mutation 覆写」,本层补足「grep/glob/
|
|
4
|
+
// mutation 覆写」,本层补足「grep/glob/web_search/web_fetch 这类观察类工具」的引用追踪。
|
|
5
5
|
//
|
|
6
6
|
// 四态机(LIVE → REFERENCED → OBSOLETE → STUB):
|
|
7
7
|
// - LIVE:刚 push 进 history 的工具结果,尚未被任何下游工具消费。
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// - STUB:已被替换为存根(物理上 content 变成「⌦[无消费者:...]」)。
|
|
11
11
|
//
|
|
12
12
|
// 观察类工具两阶段衰减(避免误伤):
|
|
13
|
-
// - grep/glob/
|
|
13
|
+
// - grep/glob/web_search/web_fetch 等「观察/检索类」工具两阶段衰减:
|
|
14
14
|
// Phase 1(10 步):LIVE → REFERENCED,保留完整内容(返回多个候选,剩余候选可能后续被消费)。
|
|
15
15
|
// Phase 2(+5 步):REFERENCED → DIGEST,替换为摘要存根(保留文件列表+命中数+参数,丢弃详情),
|
|
16
16
|
// 释放 ~90% token。states 仍为 REFERENCED,不引入新状态。
|
|
@@ -34,7 +34,6 @@ import { canonicalizePath, extractPath, isToolResultSuccess, lastUserIndex, toTe
|
|
|
34
34
|
const OBSERVER_TOOLS = new Set([
|
|
35
35
|
'grep',
|
|
36
36
|
'glob',
|
|
37
|
-
'codegraph',
|
|
38
37
|
'web_search',
|
|
39
38
|
'web_fetch',
|
|
40
39
|
]);
|
|
@@ -63,7 +62,6 @@ const OBSERVER_DIGEST_AGE = 5;
|
|
|
63
62
|
* - grep:content 是 `file:line: ...` 行,提取每行的 file 段(只保留绝对路径形态或与 pattern 匹配的)。
|
|
64
63
|
* 简化:把所有看起来像「相对路径 + 文件名」的 token 抽出,留 narrow。
|
|
65
64
|
* - glob:content 是路径列表,按行 / 空格拆。
|
|
66
|
-
* - codegraph:content 里通常含 `path/to/file.ts:line`,按行拆,提 file 段。
|
|
67
65
|
*
|
|
68
66
|
* 返回值:命中过的 path 字符串集合(已 dedup)。失败返空集。 */
|
|
69
67
|
function extractProducerPaths(toolName, content) {
|
|
@@ -99,14 +97,6 @@ function extractProducerPaths(toolName, content) {
|
|
|
99
97
|
addPath(t);
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
|
-
else if (toolName === 'codegraph') {
|
|
103
|
-
// codegraph 输出通常 `path\to\file.ts:line:col symbol` 或类似;按行 + 冒号分隔。
|
|
104
|
-
for (const line of content.split(/\r?\n/)) {
|
|
105
|
-
const m = /^([^\s:][^:]*?\.[A-Za-z0-9]+):(\d+):/.exec(line);
|
|
106
|
-
if (m)
|
|
107
|
-
addPath(m[1]);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
100
|
else if (toolName === 'web_search' || toolName === 'web_fetch') {
|
|
111
101
|
// 网络结果不在文件系统路径范畴;不参与 producer 路径索引(避免误匹配)。
|
|
112
102
|
}
|
|
@@ -287,7 +277,7 @@ export class LifecycleEngine {
|
|
|
287
277
|
})();
|
|
288
278
|
const path = canonicalizePath(extractPath(argsRaw));
|
|
289
279
|
if (path) {
|
|
290
|
-
// 1) 找该 path 的所有上游 producer(grep/glob/
|
|
280
|
+
// 1) 找该 path 的所有上游 producer(grep/glob/web_search/web_fetch)→ 标 REFERENCED。
|
|
291
281
|
const producers = this.producersByPath.get(path);
|
|
292
282
|
if (producers) {
|
|
293
283
|
for (const pidx of producers) {
|
|
@@ -478,16 +468,6 @@ export class LifecycleEngine {
|
|
|
478
468
|
} })();
|
|
479
469
|
return `${DIGEST_PREFIX}glob(${args}) — 历史结果文件 ${files},${origLen}→摘要]\n如需完整列表或最新信息请重新调用 glob`;
|
|
480
470
|
}
|
|
481
|
-
case 'codegraph': {
|
|
482
|
-
const args = (() => { try {
|
|
483
|
-
const a = JSON.parse(argsRaw);
|
|
484
|
-
return `"${a.query ?? ''}"`;
|
|
485
|
-
}
|
|
486
|
-
catch {
|
|
487
|
-
return '...';
|
|
488
|
-
} })();
|
|
489
|
-
return `${DIGEST_PREFIX}codegraph(${args}) — 历史结果文件 ${files},${origLen}→摘要]\n如需完整详情或最新信息请重新调用 codegraph`;
|
|
490
|
-
}
|
|
491
471
|
case 'web_search': {
|
|
492
472
|
// 统计结果数
|
|
493
473
|
let resultCount = 0;
|
|
@@ -35,21 +35,6 @@ function observationKey(call) {
|
|
|
35
35
|
maxPerFile: Math.min(Math.max(rawMax, 1), 50),
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
-
if (call.name === 'codegraph') {
|
|
39
|
-
if (typeof args.action !== 'string' || typeof args.query !== 'string')
|
|
40
|
-
return null;
|
|
41
|
-
const query = args.action === 'explore'
|
|
42
|
-
? args.query.trim().replace(/\s+/g, ' ')
|
|
43
|
-
: args.query.trim();
|
|
44
|
-
return JSON.stringify({
|
|
45
|
-
tool: 'codegraph',
|
|
46
|
-
action: args.action,
|
|
47
|
-
query,
|
|
48
|
-
file: typeof args.file === 'string' ? args.file.trim().replace(/\\/g, '/') : '',
|
|
49
|
-
offset: args.offset === undefined ? null : normalizedInteger(args.offset, 0),
|
|
50
|
-
limit: args.limit === undefined ? null : normalizedInteger(args.limit, 0),
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
38
|
return null;
|
|
54
39
|
}
|
|
55
40
|
function observationLabel(call) {
|
|
@@ -59,14 +44,12 @@ function observationLabel(call) {
|
|
|
59
44
|
const glob = JSON.stringify(String(args.glob ?? '**/*')).slice(0, 80);
|
|
60
45
|
return `grep(pattern=${pattern}, glob=${glob})`;
|
|
61
46
|
}
|
|
62
|
-
|
|
63
|
-
const query = JSON.stringify(String(args.query ?? '')).slice(0, 100);
|
|
64
|
-
return `codegraph(action=${action}, query=${query})`;
|
|
47
|
+
return call.name;
|
|
65
48
|
}
|
|
66
49
|
/**
|
|
67
50
|
* Cross-message relevance pruning:
|
|
68
51
|
* - read_file: a newer successful read of the same canonical path supersedes old reads.
|
|
69
|
-
* - grep
|
|
52
|
+
* - grep: a newer successful call with the exact same semantic arguments
|
|
70
53
|
* supersedes old results. Partial file overlap is intentionally not enough.
|
|
71
54
|
*/
|
|
72
55
|
export class RelevancePruner {
|
|
@@ -186,9 +169,7 @@ export class RelevancePruner {
|
|
|
186
169
|
return;
|
|
187
170
|
if (!message.tool_call_id)
|
|
188
171
|
return;
|
|
189
|
-
const reason =
|
|
190
|
-
? '相同 grep 查询已有更新结果'
|
|
191
|
-
: '相同 codegraph 查询已有更新结果';
|
|
172
|
+
const reason = '相同 grep 查询已有更新结果';
|
|
192
173
|
message.content =
|
|
193
174
|
`${STUB_PREFIX}${reason}] ${observationLabel(call)} ${content.length} 字符 ` +
|
|
194
175
|
`→ 已被更新查询替代 · id …${message.tool_call_id.slice(-6)}⌫`;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function parseAttachments(value) {
|
|
2
|
+
if (!Array.isArray(value))
|
|
3
|
+
return undefined;
|
|
4
|
+
return value.flatMap((item) => {
|
|
5
|
+
if (!item || typeof item !== 'object')
|
|
6
|
+
return [];
|
|
7
|
+
const entry = item;
|
|
8
|
+
if (typeof entry.name !== 'string' || typeof entry.dataUrl !== 'string' || !entry.dataUrl.startsWith('data:image/'))
|
|
9
|
+
return [];
|
|
10
|
+
return [{ name: entry.name.slice(0, 240), dataUrl: entry.dataUrl }];
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function parseCommand(value) {
|
|
14
|
+
if (!value || typeof value !== 'object')
|
|
15
|
+
return null;
|
|
16
|
+
const input = value;
|
|
17
|
+
if (typeof input.id !== 'string' || typeof input.type !== 'string')
|
|
18
|
+
return null;
|
|
19
|
+
if (input.type === 'run' && typeof input.prompt === 'string') {
|
|
20
|
+
return {
|
|
21
|
+
id: input.id,
|
|
22
|
+
type: 'run',
|
|
23
|
+
prompt: input.prompt,
|
|
24
|
+
sessionId: typeof input.sessionId === 'string' ? input.sessionId : undefined,
|
|
25
|
+
attachments: parseAttachments(input.attachments),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (input.type === 'cancel')
|
|
29
|
+
return { id: input.id, type: 'cancel' };
|
|
30
|
+
if (input.type === 'approval' && typeof input.approvalId === 'string') {
|
|
31
|
+
return {
|
|
32
|
+
id: input.id,
|
|
33
|
+
type: 'approval',
|
|
34
|
+
approvalId: input.approvalId,
|
|
35
|
+
action: input.action === 'selected' ? 'selected' : 'cancelled',
|
|
36
|
+
value: typeof input.value === 'string' ? input.value : undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|